Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
4f2bbba782 | |||
9eb25f6c9e | |||
f74b00446c | |||
beb7e6ec34 | |||
2eafc042ad | |||
74954bc51d | |||
6a03120225 | |||
21504573b4 | |||
fabd912fba | |||
00b42855b6 | |||
ef272360fb | |||
026a5011d4 | |||
aa4206af0e |
36
CHANGELOG.md
36
CHANGELOG.md
@ -5,6 +5,42 @@ 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.14.0 - 09.06.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added a connect or create symbol profile model logic on creating a new transaction
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the global heat map to visualize investments by country
|
||||
|
||||
## 1.13.0 - 08.06.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added a global heat map to visualize investments by country
|
||||
|
||||
## 1.12.0 - 06.06.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added a symbol profile model with additional data
|
||||
- Added new pie charts: Investments by continent and country
|
||||
|
||||
## 1.11.0 - 05.06.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added a dedicated page for the account registration
|
||||
- Rendered the average buy prices in the position detail chart (useful for recurring transactions)
|
||||
- Introduced the initial prisma migration
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed the buttons to links (`<a>`) on the tools page
|
||||
- Upgraded `prisma` from version `2.20.1` to `2.24.1`
|
||||
|
||||
## 1.10.1 - 02.06.2021
|
||||
|
||||
### Fixed
|
||||
|
@ -44,6 +44,7 @@ export class ExperimentalService {
|
||||
fee: 0,
|
||||
id: undefined,
|
||||
platformId: undefined,
|
||||
symbolProfileId: undefined,
|
||||
type: Type.BUY,
|
||||
updatedAt: undefined,
|
||||
userId: undefined
|
||||
|
@ -132,12 +132,26 @@ export class OrderController {
|
||||
return this.orderService.createOrder(
|
||||
{
|
||||
...data,
|
||||
date,
|
||||
Account: {
|
||||
connect: {
|
||||
id_userId: { id: accountId, userId: this.request.user.id }
|
||||
}
|
||||
},
|
||||
date,
|
||||
SymbolProfile: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
dataSource_symbol: {
|
||||
dataSource: data.dataSource,
|
||||
symbol: data.symbol
|
||||
}
|
||||
},
|
||||
create: {
|
||||
dataSource: data.dataSource,
|
||||
symbol: data.symbol
|
||||
}
|
||||
}
|
||||
},
|
||||
User: { connect: { id: this.request.user.id } }
|
||||
},
|
||||
this.request.user.id
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
getYear,
|
||||
isAfter,
|
||||
isSameDay,
|
||||
parse,
|
||||
parseISO,
|
||||
setDate,
|
||||
setMonth,
|
||||
@ -75,7 +76,8 @@ export class PortfolioService {
|
||||
// Get portfolio from database
|
||||
const orders = await this.orderService.orders({
|
||||
include: {
|
||||
Account: true
|
||||
Account: true,
|
||||
SymbolProfile: true
|
||||
},
|
||||
orderBy: { date: 'asc' },
|
||||
where: { userId: aUserId }
|
||||
@ -215,6 +217,8 @@ export class PortfolioService {
|
||||
transactionCount
|
||||
} = portfolio.getPositions(new Date())[aSymbol];
|
||||
|
||||
const orders = portfolio.getOrders(aSymbol);
|
||||
|
||||
const historicalData = await this.dataProviderService.getHistorical(
|
||||
[aSymbol],
|
||||
'day',
|
||||
@ -227,6 +231,7 @@ export class PortfolioService {
|
||||
}
|
||||
|
||||
const historicalDataArray: HistoricalDataItem[] = [];
|
||||
let currentAveragePrice: number;
|
||||
let maxPrice = marketPrice;
|
||||
let minPrice = marketPrice;
|
||||
|
||||
@ -234,9 +239,24 @@ export class PortfolioService {
|
||||
for (const [date, { marketPrice }] of Object.entries(
|
||||
historicalData[aSymbol]
|
||||
)) {
|
||||
const currentDate = parse(date, 'yyyy-MM-dd', new Date());
|
||||
if (
|
||||
isSameDay(currentDate, parseISO(orders[0]?.getDate())) ||
|
||||
isAfter(currentDate, parseISO(orders[0]?.getDate()))
|
||||
) {
|
||||
// Get snapshot of first day of month
|
||||
const snapshot = portfolio.get(setDate(currentDate, 1))[0]
|
||||
.positions[aSymbol];
|
||||
orders.shift();
|
||||
|
||||
if (snapshot?.averagePrice) {
|
||||
currentAveragePrice = snapshot?.averagePrice;
|
||||
}
|
||||
}
|
||||
|
||||
historicalDataArray.push({
|
||||
averagePrice,
|
||||
date,
|
||||
averagePrice: currentAveragePrice,
|
||||
value: marketPrice
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Account, Currency, Platform } from '@prisma/client';
|
||||
import { Account, Currency, Platform, SymbolProfile } from '@prisma/client';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { IOrder } from '../services/interfaces/interfaces';
|
||||
@ -12,6 +12,7 @@ export class Order {
|
||||
private id: string;
|
||||
private quantity: number;
|
||||
private symbol: string;
|
||||
private symbolProfile: SymbolProfile;
|
||||
private total: number;
|
||||
private type: OrderType;
|
||||
private unitPrice: number;
|
||||
@ -24,6 +25,7 @@ export class Order {
|
||||
this.id = data.id || uuidv4();
|
||||
this.quantity = data.quantity;
|
||||
this.symbol = data.symbol;
|
||||
this.symbolProfile = data.symbolProfile;
|
||||
this.type = data.type;
|
||||
this.unitPrice = data.unitPrice;
|
||||
|
||||
@ -58,6 +60,10 @@ export class Order {
|
||||
return this.symbol;
|
||||
}
|
||||
|
||||
getSymbolProfile() {
|
||||
return this.symbolProfile;
|
||||
}
|
||||
|
||||
public getTotal() {
|
||||
return this.total;
|
||||
}
|
||||
|
@ -189,6 +189,7 @@ describe('Portfolio', () => {
|
||||
id: '8d999347-dee2-46ee-88e1-26b344e71fcc',
|
||||
quantity: 1,
|
||||
symbol: 'BTCUSD',
|
||||
symbolProfileId: null,
|
||||
type: Type.BUY,
|
||||
unitPrice: 49631.24,
|
||||
updatedAt: null,
|
||||
@ -223,6 +224,7 @@ describe('Portfolio', () => {
|
||||
},
|
||||
allocationCurrent: 1,
|
||||
allocationInvestment: 1,
|
||||
countries: [],
|
||||
currency: Currency.USD,
|
||||
exchange: UNKNOWN_KEY,
|
||||
grossPerformance: 0,
|
||||
@ -290,6 +292,7 @@ describe('Portfolio', () => {
|
||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
|
||||
quantity: 0.2,
|
||||
symbol: 'ETHUSD',
|
||||
symbolProfileId: null,
|
||||
type: Type.BUY,
|
||||
unitPrice: 991.49,
|
||||
updatedAt: null,
|
||||
@ -324,6 +327,7 @@ describe('Portfolio', () => {
|
||||
},
|
||||
// allocationCurrent: 1,
|
||||
allocationInvestment: 1,
|
||||
countries: [],
|
||||
currency: Currency.USD,
|
||||
exchange: UNKNOWN_KEY,
|
||||
// grossPerformance: 0,
|
||||
@ -385,6 +389,7 @@ describe('Portfolio', () => {
|
||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
|
||||
quantity: 0.2,
|
||||
symbol: 'ETHUSD',
|
||||
symbolProfileId: null,
|
||||
type: Type.BUY,
|
||||
unitPrice: 991.49,
|
||||
updatedAt: null,
|
||||
@ -401,6 +406,7 @@ describe('Portfolio', () => {
|
||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fc',
|
||||
quantity: 0.3,
|
||||
symbol: 'ETHUSD',
|
||||
symbolProfileId: null,
|
||||
type: Type.BUY,
|
||||
unitPrice: 1050,
|
||||
updatedAt: null,
|
||||
@ -461,6 +467,7 @@ describe('Portfolio', () => {
|
||||
id: 'd96795b2-6ae6-420e-aa21-fabe5e45d475',
|
||||
quantity: 0.05614682,
|
||||
symbol: 'BTCUSD',
|
||||
symbolProfileId: null,
|
||||
type: Type.BUY,
|
||||
unitPrice: 3562.089535970158,
|
||||
updatedAt: null,
|
||||
@ -477,6 +484,7 @@ describe('Portfolio', () => {
|
||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
|
||||
quantity: 0.2,
|
||||
symbol: 'ETHUSD',
|
||||
symbolProfileId: null,
|
||||
type: Type.BUY,
|
||||
unitPrice: 991.49,
|
||||
updatedAt: null,
|
||||
@ -550,6 +558,7 @@ describe('Portfolio', () => {
|
||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
|
||||
quantity: 0.2,
|
||||
symbol: 'ETHUSD',
|
||||
symbolProfileId: null,
|
||||
type: Type.BUY,
|
||||
unitPrice: 991.49,
|
||||
updatedAt: null,
|
||||
@ -566,6 +575,7 @@ describe('Portfolio', () => {
|
||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fc',
|
||||
quantity: 0.1,
|
||||
symbol: 'ETHUSD',
|
||||
symbolProfileId: null,
|
||||
type: Type.SELL,
|
||||
unitPrice: 1050,
|
||||
updatedAt: null,
|
||||
@ -582,6 +592,7 @@ describe('Portfolio', () => {
|
||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fc',
|
||||
quantity: 0.2,
|
||||
symbol: 'ETHUSD',
|
||||
symbolProfileId: null,
|
||||
type: Type.BUY,
|
||||
unitPrice: 1050,
|
||||
updatedAt: null,
|
||||
|
@ -8,7 +8,10 @@ import {
|
||||
Position,
|
||||
UserWithSettings
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
||||
import { DateRange, OrderWithAccount } from '@ghostfolio/common/types';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { continents, countries } from 'countries-list';
|
||||
import {
|
||||
add,
|
||||
format,
|
||||
@ -127,6 +130,7 @@ export class Portfolio implements PortfolioInterface {
|
||||
id,
|
||||
quantity,
|
||||
symbol,
|
||||
symbolProfile,
|
||||
type,
|
||||
unitPrice
|
||||
}) => {
|
||||
@ -139,6 +143,7 @@ export class Portfolio implements PortfolioInterface {
|
||||
id,
|
||||
quantity,
|
||||
symbol,
|
||||
symbolProfile,
|
||||
type,
|
||||
unitPrice
|
||||
})
|
||||
@ -204,6 +209,7 @@ export class Portfolio implements PortfolioInterface {
|
||||
|
||||
symbols.forEach((symbol) => {
|
||||
const accounts: PortfolioPosition['accounts'] = {};
|
||||
let countriesOfSymbol: Country[];
|
||||
const [portfolioItem] = portfolioItems;
|
||||
|
||||
const ordersBySymbol = this.getOrders().filter((order) => {
|
||||
@ -243,6 +249,21 @@ export class Portfolio implements PortfolioInterface {
|
||||
original: originalValueOfSymbol
|
||||
};
|
||||
}
|
||||
|
||||
countriesOfSymbol = (
|
||||
(orderOfSymbol.getSymbolProfile()?.countries as Prisma.JsonArray) ??
|
||||
[]
|
||||
).map((country) => {
|
||||
const { code, weight } = country as Prisma.JsonObject;
|
||||
|
||||
return {
|
||||
code: code as string,
|
||||
continent:
|
||||
continents[countries[code as string]?.continent] ?? UNKNOWN_KEY,
|
||||
name: countries[code as string]?.name ?? UNKNOWN_KEY,
|
||||
weight: weight as number
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
let now = portfolioItemsNow.positions[symbol].marketPrice;
|
||||
@ -289,6 +310,7 @@ export class Portfolio implements PortfolioInterface {
|
||||
) / value,
|
||||
allocationInvestment:
|
||||
portfolioItem.positions[symbol].investment / investment,
|
||||
countries: countriesOfSymbol,
|
||||
grossPerformance: roundTo(
|
||||
portfolioItemsNow.positions[symbol].quantity * (now - before),
|
||||
2
|
||||
@ -296,7 +318,12 @@ export class Portfolio implements PortfolioInterface {
|
||||
grossPerformancePercent: roundTo((now - before) / before, 4),
|
||||
investment: portfolioItem.positions[symbol].investment,
|
||||
quantity: portfolioItem.positions[symbol].quantity,
|
||||
transactionCount: portfolioItem.positions[symbol].transactionCount
|
||||
transactionCount: portfolioItem.positions[symbol].transactionCount,
|
||||
value: this.exchangeRateDataService.toCurrency(
|
||||
portfolioItem.positions[symbol].quantity * now,
|
||||
data[symbol]?.currency,
|
||||
this.user.Settings.currency
|
||||
)
|
||||
};
|
||||
});
|
||||
|
||||
@ -486,7 +513,13 @@ export class Portfolio implements PortfolioInterface {
|
||||
.reduce((previous, current) => previous + current, 0);
|
||||
}
|
||||
|
||||
public getOrders() {
|
||||
public getOrders(aSymbol?: string) {
|
||||
if (aSymbol) {
|
||||
return this.orders.filter((order) => {
|
||||
return order.getSymbol() === aSymbol;
|
||||
});
|
||||
}
|
||||
|
||||
return this.orders;
|
||||
}
|
||||
|
||||
@ -538,6 +571,7 @@ export class Portfolio implements PortfolioInterface {
|
||||
fee: order.fee,
|
||||
quantity: order.quantity,
|
||||
symbol: order.symbol,
|
||||
symbolProfile: order.SymbolProfile,
|
||||
type: <OrderType>order.type,
|
||||
unitPrice: order.unitPrice
|
||||
})
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||
import { Account, Currency, DataSource } from '@prisma/client';
|
||||
import { Account, Currency, DataSource, SymbolProfile } from '@prisma/client';
|
||||
|
||||
import { OrderType } from '../../models/order-type';
|
||||
|
||||
@ -41,6 +41,7 @@ export interface IOrder {
|
||||
id?: string;
|
||||
quantity: number;
|
||||
symbol: string;
|
||||
symbolProfile: SymbolProfile;
|
||||
type: OrderType;
|
||||
unitPrice: number;
|
||||
}
|
||||
|
@ -45,6 +45,13 @@ const routes: Routes = [
|
||||
(m) => m.PricingPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'register',
|
||||
loadChildren: () =>
|
||||
import('./pages/register/register-page.module').then(
|
||||
(m) => m.RegisterPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'resources',
|
||||
loadChildren: () =>
|
||||
@ -55,7 +62,9 @@ const routes: Routes = [
|
||||
{
|
||||
path: 'start',
|
||||
loadChildren: () =>
|
||||
import('./pages/login/login-page.module').then((m) => m.LoginPageModule)
|
||||
import('./pages/landing/landing-page.module').then(
|
||||
(m) => m.LandingPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'tools',
|
||||
|
@ -12,13 +12,15 @@
|
||||
<div *ngIf="canCreateAccount" class="container create-account-container">
|
||||
<div class="row mb-5">
|
||||
<div class="col-md-6 offset-md-3">
|
||||
<div
|
||||
class="create-account-box p-2 text-center"
|
||||
(click)="onCreateAccount()"
|
||||
<a [routerLink]="['/']">
|
||||
<mat-card
|
||||
class="create-account-box p-2 text-center"
|
||||
(click)="onCreateAccount()"
|
||||
>
|
||||
<div class="mt-1" i18n>You are using the Live Demo.</div>
|
||||
<button mat-button color="primary" i18n>Create Account</button>
|
||||
</mat-card></a
|
||||
>
|
||||
<div class="mt-1" i18n>You are using the Live Demo.</div>
|
||||
<button mat-button color="primary" i18n>Create Account</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,8 +5,6 @@
|
||||
padding: 5rem 0;
|
||||
|
||||
.create-account-box {
|
||||
border: 1px solid rgba(var(--palette-primary-500), 1);
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
font-size: 90%;
|
||||
|
||||
|
@ -68,17 +68,12 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
this.user = state.user;
|
||||
|
||||
this.canCreateAccount = hasPermission(
|
||||
this.user.permissions,
|
||||
permissions.createUserAccount
|
||||
);
|
||||
} else if (!this.tokenStorageService.getToken()) {
|
||||
// User has not been logged in
|
||||
this.user = null;
|
||||
}
|
||||
this.canCreateAccount = hasPermission(
|
||||
this.user?.permissions,
|
||||
permissions.createUserAccount
|
||||
);
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
@ -86,7 +81,6 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
|
||||
public onCreateAccount() {
|
||||
this.tokenStorageService.signOut();
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
public onSignOut() {
|
||||
|
@ -2,6 +2,7 @@ import { Platform } from '@angular/cdk/platform';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import {
|
||||
DateAdapter,
|
||||
MAT_DATE_FORMATS,
|
||||
@ -34,6 +35,7 @@ import { LanguageService } from './core/language.service';
|
||||
HttpClientModule,
|
||||
MarkdownModule.forRoot(),
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MaterialCssVarsModule.forRoot({
|
||||
darkThemeClass: 'is-dark-theme',
|
||||
isAutoContrast: true,
|
||||
|
@ -8,9 +8,11 @@
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[color]="
|
||||
currentRoute === 'home' || currentRoute === 'zen' ? 'primary' : null
|
||||
"
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'home' || currentRoute === 'zen',
|
||||
'text-decoration-underline':
|
||||
currentRoute === 'home' || currentRoute === 'zen'
|
||||
}"
|
||||
[routerLink]="['/']"
|
||||
>Overview</a
|
||||
>
|
||||
@ -19,13 +21,16 @@
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[color]="
|
||||
currentRoute === 'analysis' ||
|
||||
currentRoute === 'report' ||
|
||||
currentRoute === 'tools'
|
||||
? 'primary'
|
||||
: null
|
||||
"
|
||||
[ngClass]="{
|
||||
'font-weight-bold':
|
||||
currentRoute === 'analysis' ||
|
||||
currentRoute === 'report' ||
|
||||
currentRoute === 'tools',
|
||||
'text-decoration-underline':
|
||||
currentRoute === 'analysis' ||
|
||||
currentRoute === 'report' ||
|
||||
currentRoute === 'tools'
|
||||
}"
|
||||
[routerLink]="['/tools']"
|
||||
>Tools</a
|
||||
>
|
||||
@ -33,7 +38,10 @@
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[color]="currentRoute === 'transactions' ? 'primary' : null"
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'transactions',
|
||||
'text-decoration-underline': currentRoute === 'transactions'
|
||||
}"
|
||||
[routerLink]="['/transactions']"
|
||||
>Transactions</a
|
||||
>
|
||||
@ -41,7 +49,10 @@
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[color]="currentRoute === 'accounts' ? 'primary' : null"
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'accounts',
|
||||
'text-decoration-underline': currentRoute === 'accounts'
|
||||
}"
|
||||
[routerLink]="['/accounts']"
|
||||
>Accounts</a
|
||||
>
|
||||
@ -50,7 +61,10 @@
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[color]="currentRoute === 'admin' ? 'primary' : null"
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'admin',
|
||||
'text-decoration-underline': currentRoute === 'admin'
|
||||
}"
|
||||
[routerLink]="['/admin']"
|
||||
>Admin Control</a
|
||||
>
|
||||
@ -58,7 +72,10 @@
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[color]="currentRoute === 'resources' ? 'primary' : null"
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'resources',
|
||||
'text-decoration-underline': currentRoute === 'resources'
|
||||
}"
|
||||
[routerLink]="['/resources']"
|
||||
>Resources</a
|
||||
>
|
||||
@ -67,7 +84,10 @@
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[color]="currentRoute === 'pricing' ? 'primary' : null"
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'pricing',
|
||||
'text-decoration-underline': currentRoute === 'pricing'
|
||||
}"
|
||||
[routerLink]="['/pricing']"
|
||||
>Pricing</a
|
||||
>
|
||||
@ -75,7 +95,10 @@
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[color]="currentRoute === 'about' ? 'primary' : null"
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'about',
|
||||
'text-decoration-underline': currentRoute === 'about'
|
||||
}"
|
||||
[routerLink]="['/about']"
|
||||
>About</a
|
||||
>
|
||||
@ -226,28 +249,44 @@
|
||||
<gf-logo></gf-logo>
|
||||
</a>
|
||||
<span class="spacer"></span>
|
||||
<a
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[color]="currentRoute === 'pricing' ? 'primary' : null"
|
||||
[routerLink]="['/pricing']"
|
||||
>Pricing</a
|
||||
>
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[color]="currentRoute === 'about' ? 'primary' : null"
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'about',
|
||||
'text-decoration-underline': currentRoute === 'about'
|
||||
}"
|
||||
[routerLink]="['/about']"
|
||||
>About</a
|
||||
>
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'pricing',
|
||||
'text-decoration-underline': currentRoute === 'pricing'
|
||||
}"
|
||||
[routerLink]="['/pricing']"
|
||||
>Pricing</a
|
||||
>
|
||||
<a
|
||||
class="d-none d-sm-block mx-1 no-min-width px-1"
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
mat-flat-button
|
||||
>GitHub</a
|
||||
>
|
||||
<button i18n mat-flat-button (click)="openLoginDialog()">Sign in</button>
|
||||
><ion-icon name="logo-github"></ion-icon
|
||||
></a>
|
||||
<button class="mx-1" i18n mat-flat-button (click)="openLoginDialog()">
|
||||
Sign In
|
||||
</button>
|
||||
<a
|
||||
class="d-none d-sm-block"
|
||||
color="primary"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[routerLink]="['/register']"
|
||||
>Get Started
|
||||
</a>
|
||||
</ng-container>
|
||||
</mat-toolbar>
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
} from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Router } from '@angular/router';
|
||||
import { LoginWithAccessTokenDialog } from '@ghostfolio/client/pages/login/login-with-access-token-dialog/login-with-access-token-dialog.component';
|
||||
import { LoginWithAccessTokenDialog } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.component';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
|
@ -4,7 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { LoginWithAccessTokenDialogModule } from '@ghostfolio/client/pages/login/login-with-access-token-dialog/login-with-access-token-dialog.module';
|
||||
import { LoginWithAccessTokenDialogModule } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.module';
|
||||
|
||||
import { GfLogoModule } from '../logo/logo.module';
|
||||
import { HeaderComponent } from './header.component';
|
||||
|
@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
|
||||
@Component({
|
||||
selector: 'login-with-access-token-dialog',
|
||||
selector: 'gf-login-with-access-token-dialog',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
styleUrls: ['./login-with-access-token-dialog.scss'],
|
||||
templateUrl: 'login-with-access-token-dialog.html'
|
@ -0,0 +1,10 @@
|
||||
<ngx-skeleton-loader
|
||||
*ngIf="isLoading"
|
||||
animation="pulse"
|
||||
class="h-100"
|
||||
[theme]="{
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
|
||||
<div class="align-items-center d-flex h-100 w-100" id="svgMap"></div>
|
@ -0,0 +1,32 @@
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
|
||||
::ng-deep {
|
||||
.loader {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.svgMap-map-wrapper {
|
||||
background: transparent;
|
||||
|
||||
.svgMap-country {
|
||||
stroke: #e5e5e5;
|
||||
}
|
||||
|
||||
.svgMap-map-controls-wrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
::ng-deep {
|
||||
.svgMap-map-wrapper {
|
||||
.svgMap-country {
|
||||
stroke: #414141;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { Currency } from '@prisma/client';
|
||||
import svgMap from 'svgmap';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-world-map-chart',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './world-map-chart.component.html',
|
||||
styleUrls: ['./world-map-chart.component.scss']
|
||||
})
|
||||
export class WorldMapChartComponent implements OnChanges, OnDestroy, OnInit {
|
||||
@Input() baseCurrency: Currency;
|
||||
@Input() countries: { [code: string]: { name: string; value: number } };
|
||||
|
||||
public isLoading = true;
|
||||
public svgMapElement;
|
||||
|
||||
public constructor(private changeDetectorRef: ChangeDetectorRef) {}
|
||||
|
||||
public ngOnInit() {}
|
||||
|
||||
public ngOnChanges() {
|
||||
if (this.countries) {
|
||||
this.isLoading = true;
|
||||
|
||||
this.destroySvgMap();
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.destroySvgMap();
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
this.svgMapElement = new svgMap({
|
||||
colorMax: '#22bdb9',
|
||||
colorMin: '#c3f1f0',
|
||||
colorNoData: 'transparent',
|
||||
data: {
|
||||
applyData: 'value',
|
||||
data: {
|
||||
value: {
|
||||
format: `{0} ${this.baseCurrency}`
|
||||
}
|
||||
},
|
||||
values: this.countries
|
||||
},
|
||||
hideFlag: true,
|
||||
minZoom: 1.06,
|
||||
maxZoom: 1.06,
|
||||
targetElementID: 'svgMap'
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.isLoading = false;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private destroySvgMap() {
|
||||
this.svgMapElement?.mapWrapper?.remove();
|
||||
this.svgMapElement?.tooltip?.remove();
|
||||
|
||||
this.svgMapElement = null;
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
import { WorldMapChartComponent } from './world-map-chart.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [WorldMapChartComponent],
|
||||
exports: [WorldMapChartComponent],
|
||||
imports: [CommonModule, NgxSkeletonLoaderModule],
|
||||
providers: []
|
||||
})
|
||||
export class GfWorldMapChartModule {}
|
@ -14,7 +14,12 @@ import { UserService } from '../services/user/user.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AuthGuard implements CanActivate {
|
||||
private static PUBLIC_PAGE_ROUTES = ['/about', '/pricing', '/resources'];
|
||||
private static PUBLIC_PAGE_ROUTES = [
|
||||
'/about',
|
||||
'/pricing',
|
||||
'/register',
|
||||
'/resources'
|
||||
];
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
|
@ -80,7 +80,10 @@ export class AdminPageComponent implements OnInit {
|
||||
addSuffix: true
|
||||
});
|
||||
|
||||
return distanceString === '0 seconds ago' ? 'just now' : distanceString;
|
||||
return distanceString === 'in 0 seconds' ||
|
||||
distanceString === '0 seconds ago'
|
||||
? 'just now'
|
||||
: distanceString;
|
||||
}
|
||||
|
||||
return '';
|
||||
|
@ -2,14 +2,14 @@ import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { LoginPageComponent } from './login-page.component';
|
||||
import { LandingPageComponent } from './landing-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: LoginPageComponent, canActivate: [AuthGuard] }
|
||||
{ path: '', component: LandingPageComponent, canActivate: [AuthGuard] }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class LoginPageRoutingModule {}
|
||||
export class LandingPageRoutingModule {}
|
@ -1,21 +1,17 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Router } from '@angular/router';
|
||||
import { LineChartItem } from '@ghostfolio/client/components/line-chart/interfaces/line-chart.interface';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import { format } from 'date-fns';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { ShowAccessTokenDialog } from './show-access-token-dialog/show-access-token-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-login-page',
|
||||
templateUrl: './login-page.html',
|
||||
styleUrls: ['./login-page.scss']
|
||||
selector: 'gf-landing-page',
|
||||
templateUrl: './landing-page.html',
|
||||
styleUrls: ['./landing-page.scss']
|
||||
})
|
||||
export class LoginPageComponent implements OnDestroy, OnInit {
|
||||
export class LandingPageComponent implements OnDestroy, OnInit {
|
||||
public currentYear = format(new Date(), 'yyyy');
|
||||
public demoAuthToken: string;
|
||||
public historicalDataItems: LineChartItem[];
|
||||
@ -28,7 +24,6 @@ export class LoginPageComponent implements OnDestroy, OnInit {
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private dialog: MatDialog,
|
||||
private router: Router,
|
||||
private tokenStorageService: TokenStorageService
|
||||
) {}
|
||||
@ -46,15 +41,6 @@ export class LoginPageComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public async createAccount() {
|
||||
this.dataService
|
||||
.postUser()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ accessToken, authToken }) => {
|
||||
this.openShowAccessTokenDialog(accessToken, authToken);
|
||||
});
|
||||
}
|
||||
|
||||
public initializeLineChart() {
|
||||
this.historicalDataItems = [
|
||||
{
|
||||
@ -268,28 +254,6 @@ export class LoginPageComponent implements OnDestroy, OnInit {
|
||||
];
|
||||
}
|
||||
|
||||
public openShowAccessTokenDialog(
|
||||
accessToken: string,
|
||||
authToken: string
|
||||
): void {
|
||||
const dialogRef = this.dialog.open(ShowAccessTokenDialog, {
|
||||
data: {
|
||||
accessToken,
|
||||
authToken
|
||||
},
|
||||
disableClose: true,
|
||||
width: '30rem'
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((data) => {
|
||||
if (data?.authToken) {
|
||||
this.tokenStorageService.saveToken(authToken);
|
||||
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public setToken(aToken: string) {
|
||||
this.tokenStorageService.saveToken(aToken);
|
||||
|
@ -13,16 +13,16 @@
|
||||
class="align-items-center col d-flex justify-content-center position-relative"
|
||||
>
|
||||
<div class="py-5 text-center">
|
||||
<button
|
||||
<a
|
||||
class="d-inline-block"
|
||||
color="primary"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[disabled]="!demoAuthToken"
|
||||
(click)="createAccount()"
|
||||
[routerLink]="['/register']"
|
||||
>
|
||||
Create Account
|
||||
</button>
|
||||
Get Started
|
||||
</a>
|
||||
<div class="d-inline-block mx-3 text-muted" i18n>or</div>
|
||||
<button
|
||||
class="d-inline-block"
|
||||
@ -135,15 +135,15 @@
|
||||
Join now or check out the example account
|
||||
</p>
|
||||
<div class="py-2 text-center">
|
||||
<button
|
||||
<a
|
||||
color="primary"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[disabled]="!demoAuthToken"
|
||||
(click)="createAccount()"
|
||||
[routerLink]="['/register']"
|
||||
>
|
||||
Create Account
|
||||
</button>
|
||||
Get Started
|
||||
</a>
|
||||
<div class="d-inline-block mx-3 text-muted" i18n>or</div>
|
||||
<button
|
||||
class="d-inline-block"
|
25
apps/client/src/app/pages/landing/landing-page.module.ts
Normal file
25
apps/client/src/app/pages/landing/landing-page.module.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
||||
import { GfLogoModule } from '@ghostfolio/client/components/logo/logo.module';
|
||||
|
||||
import { LandingPageRoutingModule } from './landing-page-routing.module';
|
||||
import { LandingPageComponent } from './landing-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [LandingPageComponent],
|
||||
exports: [],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfLineChartModule,
|
||||
GfLogoModule,
|
||||
LandingPageRoutingModule,
|
||||
MatButtonModule,
|
||||
RouterModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class LandingPageModule {}
|
@ -1,7 +1,9 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3 class="d-flex justify-content-center mb-3" i18n>Pricing Plans</h3>
|
||||
<h3 class="d-flex justify-content-center mb-3 text-center" i18n>
|
||||
Pricing Plans
|
||||
</h3>
|
||||
<mat-card class="mb-4">
|
||||
<mat-card-content>
|
||||
<p>
|
||||
@ -188,8 +190,8 @@
|
||||
</div>
|
||||
<div *ngIf="!user" class="row">
|
||||
<div class="col mt-3 text-center">
|
||||
<a color="primary" i18n mat-flat-button [routerLink]="['/start']">
|
||||
Create Account
|
||||
<a color="primary" i18n mat-flat-button [routerLink]="['/register']">
|
||||
Get Started
|
||||
</a>
|
||||
<p class="text-muted"><small>It's free</small></p>
|
||||
</div>
|
||||
|
@ -0,0 +1,15 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { RegisterPageComponent } from './register-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: RegisterPageComponent, canActivate: [AuthGuard] }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class RegisterPageRoutingModule {}
|
@ -0,0 +1,98 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Router } from '@angular/router';
|
||||
import { LineChartItem } from '@ghostfolio/client/components/line-chart/interfaces/line-chart.interface';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { format } from 'date-fns';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { ShowAccessTokenDialog } from './show-access-token-dialog/show-access-token-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-register-page',
|
||||
templateUrl: './register-page.html',
|
||||
styleUrls: ['./register-page.scss']
|
||||
})
|
||||
export class RegisterPageComponent implements OnDestroy, OnInit {
|
||||
public currentYear = format(new Date(), 'yyyy');
|
||||
public demoAuthToken: string;
|
||||
public hasPermissionForSocialLogin: boolean;
|
||||
public historicalDataItems: LineChartItem[];
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private dialog: MatDialog,
|
||||
private router: Router,
|
||||
private tokenStorageService: TokenStorageService
|
||||
) {
|
||||
this.tokenStorageService.signOut();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
*/
|
||||
public ngOnInit() {
|
||||
this.dataService
|
||||
.fetchInfo()
|
||||
.subscribe(({ demoAuthToken, globalPermissions }) => {
|
||||
this.demoAuthToken = demoAuthToken;
|
||||
this.hasPermissionForSocialLogin = hasPermission(
|
||||
globalPermissions,
|
||||
permissions.enableSocialLogin
|
||||
);
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
public async createAccount() {
|
||||
this.dataService
|
||||
.postUser()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ accessToken, authToken }) => {
|
||||
this.openShowAccessTokenDialog(accessToken, authToken);
|
||||
});
|
||||
}
|
||||
|
||||
public openShowAccessTokenDialog(
|
||||
accessToken: string,
|
||||
authToken: string
|
||||
): void {
|
||||
const dialogRef = this.dialog.open(ShowAccessTokenDialog, {
|
||||
data: {
|
||||
accessToken,
|
||||
authToken
|
||||
},
|
||||
disableClose: true,
|
||||
width: '30rem'
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((data) => {
|
||||
if (data?.authToken) {
|
||||
this.tokenStorageService.saveToken(authToken);
|
||||
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public setToken(aToken: string) {
|
||||
this.tokenStorageService.saveToken(aToken);
|
||||
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
}
|
30
apps/client/src/app/pages/register/register-page.html
Normal file
30
apps/client/src/app/pages/register/register-page.html
Normal file
@ -0,0 +1,30 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3 class="d-flex justify-content-center mb-3 text-center" i18n>
|
||||
Create your Ghostfolio account
|
||||
</h3>
|
||||
<mat-card class="mb-4">
|
||||
<mat-card-content class="text-center">
|
||||
<button
|
||||
class="d-inline-block"
|
||||
color="primary"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[disabled]="!demoAuthToken"
|
||||
(click)="createAccount()"
|
||||
>
|
||||
Create Account
|
||||
</button>
|
||||
<ng-container *ngIf="hasPermissionForSocialLogin">
|
||||
<div class="my-3 text-muted" i18n>or</div>
|
||||
<a color="accent" href="/api/auth/google" mat-flat-button
|
||||
><ion-icon class="mr-1" name="logo-google"></ion-icon
|
||||
><span i18n>Continue with Google</span></a
|
||||
>
|
||||
</ng-container>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,27 +1,27 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
||||
import { GfLogoModule } from '@ghostfolio/client/components/logo/logo.module';
|
||||
|
||||
import { LoginPageRoutingModule } from './login-page-routing.module';
|
||||
import { LoginPageComponent } from './login-page.component';
|
||||
import { RegisterPageRoutingModule } from './register-page-routing.module';
|
||||
import { RegisterPageComponent } from './register-page.component';
|
||||
import { ShowAccessTokenDialogModule } from './show-access-token-dialog/show-access-token-dialog.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [LoginPageComponent],
|
||||
declarations: [RegisterPageComponent],
|
||||
exports: [],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfLineChartModule,
|
||||
GfLogoModule,
|
||||
LoginPageRoutingModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
RegisterPageRoutingModule,
|
||||
RouterModule,
|
||||
ShowAccessTokenDialogModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class LoginPageModule {}
|
||||
export class RegisterPageModule {}
|
3
apps/client/src/app/pages/register/register-page.scss
Normal file
3
apps/client/src/app/pages/register/register-page.scss
Normal file
@ -0,0 +1,3 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
@ -7,7 +7,7 @@ import {
|
||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
|
||||
@Component({
|
||||
selector: 'show-access-token-dialog',
|
||||
selector: 'gf-show-access-token-dialog',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
styleUrls: ['./show-access-token-dialog.scss'],
|
||||
templateUrl: 'show-access-token-dialog.html'
|
@ -1,5 +1,5 @@
|
||||
import { ClipboardModule } from '@angular/cdk/clipboard';
|
||||
import { CdkTextareaAutosize, TextFieldModule } from '@angular/cdk/text-field';
|
||||
import { TextFieldModule } from '@angular/cdk/text-field';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
@ -3,6 +3,7 @@ import { ToggleOption } from '@ghostfolio/client/components/toggle/interfaces/to
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||
import {
|
||||
PortfolioItem,
|
||||
PortfolioPosition,
|
||||
@ -21,6 +22,12 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
public accounts: {
|
||||
[symbol: string]: Pick<PortfolioPosition, 'name'> & { value: number };
|
||||
};
|
||||
public continents: {
|
||||
[code: string]: { name: string; value: number };
|
||||
};
|
||||
public countries: {
|
||||
[code: string]: { name: string; value: number };
|
||||
};
|
||||
public deviceType: string;
|
||||
public period = 'current';
|
||||
public periodOptions: ToggleOption[] = [
|
||||
@ -97,6 +104,18 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
aPeriod: string
|
||||
) {
|
||||
this.accounts = {};
|
||||
this.continents = {
|
||||
[UNKNOWN_KEY]: {
|
||||
name: UNKNOWN_KEY,
|
||||
value: 0
|
||||
}
|
||||
};
|
||||
this.countries = {
|
||||
[UNKNOWN_KEY]: {
|
||||
name: UNKNOWN_KEY,
|
||||
value: 0
|
||||
}
|
||||
};
|
||||
this.positions = {};
|
||||
this.positionsArray = [];
|
||||
|
||||
@ -122,11 +141,53 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
aPeriod === 'original' ? original : current;
|
||||
} else {
|
||||
this.accounts[account] = {
|
||||
value: aPeriod === 'original' ? original : current,
|
||||
name: account
|
||||
name: account,
|
||||
value: aPeriod === 'original' ? original : current
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (position.countries.length > 0) {
|
||||
for (const country of position.countries) {
|
||||
const { code, continent, name, weight } = country;
|
||||
|
||||
if (this.continents[continent]?.value) {
|
||||
this.continents[continent].value += weight * position.value;
|
||||
} else {
|
||||
this.continents[continent] = {
|
||||
name: continent,
|
||||
value:
|
||||
weight *
|
||||
(aPeriod === 'original'
|
||||
? this.portfolioPositions[symbol].investment
|
||||
: this.portfolioPositions[symbol].value)
|
||||
};
|
||||
}
|
||||
|
||||
if (this.countries[code]?.value) {
|
||||
this.countries[code].value += weight * position.value;
|
||||
} else {
|
||||
this.countries[code] = {
|
||||
name,
|
||||
value:
|
||||
weight *
|
||||
(aPeriod === 'original'
|
||||
? this.portfolioPositions[symbol].investment
|
||||
: this.portfolioPositions[symbol].value)
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.continents[UNKNOWN_KEY].value +=
|
||||
aPeriod === 'original'
|
||||
? this.portfolioPositions[symbol].investment
|
||||
: this.portfolioPositions[symbol].value;
|
||||
|
||||
this.countries[UNKNOWN_KEY].value +=
|
||||
aPeriod === 'original'
|
||||
? this.portfolioPositions[symbol].investment
|
||||
: this.portfolioPositions[symbol].value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -146,6 +146,50 @@
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-header class="w-100">
|
||||
<mat-card-title i18n>By Continent</mat-card-title>
|
||||
<gf-toggle
|
||||
[defaultValue]="period"
|
||||
[isLoading]="false"
|
||||
[options]="periodOptions"
|
||||
(change)="onChangePeriod($event.value)"
|
||||
></gf-toggle>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<gf-portfolio-proportion-chart
|
||||
key="name"
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[isInPercent]="false"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="continents"
|
||||
></gf-portfolio-proportion-chart>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-header class="w-100">
|
||||
<mat-card-title i18n>By Country</mat-card-title>
|
||||
<gf-toggle
|
||||
[defaultValue]="period"
|
||||
[isLoading]="false"
|
||||
[options]="periodOptions"
|
||||
(change)="onChangePeriod($event.value)"
|
||||
></gf-toggle>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<gf-portfolio-proportion-chart
|
||||
key="name"
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[isInPercent]="false"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="countries"
|
||||
></gf-portfolio-proportion-chart>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-block d-sm-none row">
|
||||
<div class="col-lg">
|
||||
@ -167,12 +211,33 @@
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-none d-sm-block row">
|
||||
<div class="row world-map-chart">
|
||||
<div class="col-lg">
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-header class="w-100">
|
||||
<mat-card-title i18n>Regions</mat-card-title>
|
||||
<gf-toggle
|
||||
[defaultValue]="period"
|
||||
[isLoading]="false"
|
||||
[options]="periodOptions"
|
||||
(change)="onChangePeriod($event.value)"
|
||||
></gf-toggle>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<gf-world-map-chart
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[countries]="countries"
|
||||
></gf-world-map-chart>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg">
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title class="align-items-center d-flex" i18n
|
||||
>Investment</mat-card-title
|
||||
>Timeline</mat-card-title
|
||||
>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
|
@ -6,6 +6,7 @@ import { PortfolioPositionsChartModule } from '@ghostfolio/client/components/por
|
||||
import { PortfolioProportionChartModule } from '@ghostfolio/client/components/portfolio-proportion-chart/portfolio-proportion-chart.module';
|
||||
import { GfPositionsTableModule } from '@ghostfolio/client/components/positions-table/positions-table.module';
|
||||
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
||||
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
|
||||
|
||||
import { AnalysisPageRoutingModule } from './analysis-page-routing.module';
|
||||
import { AnalysisPageComponent } from './analysis-page.component';
|
||||
@ -19,6 +20,7 @@ import { AnalysisPageComponent } from './analysis-page.component';
|
||||
GfInvestmentChartModule,
|
||||
GfPositionsTableModule,
|
||||
GfToggleModule,
|
||||
GfWorldMapChartModule,
|
||||
MatCardModule,
|
||||
PortfolioPositionsChartModule,
|
||||
PortfolioProportionChartModule
|
||||
|
@ -7,6 +7,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.world-map-chart {
|
||||
.mat-card {
|
||||
.mat-card-content {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mat-card {
|
||||
.mat-card-header {
|
||||
::ng-deep {
|
||||
|
@ -9,14 +9,14 @@
|
||||
portfolio.
|
||||
</p>
|
||||
<p class="text-right">
|
||||
<button
|
||||
<a
|
||||
color="primary"
|
||||
i18n
|
||||
mat-button
|
||||
[routerLink]="['/tools', 'analysis']"
|
||||
>
|
||||
Open Analysis →
|
||||
</button>
|
||||
</a>
|
||||
</p>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -28,14 +28,14 @@
|
||||
risks in your portfolio.
|
||||
</p>
|
||||
<p class="text-right">
|
||||
<button
|
||||
<a
|
||||
color="primary"
|
||||
i18n
|
||||
mat-button
|
||||
[routerLink]="['/tools', 'report']"
|
||||
>
|
||||
Open X-ray →
|
||||
</button>
|
||||
</a>
|
||||
</p>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { UserService } from './user/user.service';
|
||||
|
||||
const TOKEN_KEY = 'auth-token';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TokenStorageService {
|
||||
public constructor() {}
|
||||
public constructor(private userService: UserService) {}
|
||||
|
||||
public getToken(): string {
|
||||
return window.localStorage.getItem(TOKEN_KEY);
|
||||
@ -22,6 +24,8 @@ export class TokenStorageService {
|
||||
|
||||
window.localStorage.clear();
|
||||
|
||||
this.userService.remove();
|
||||
|
||||
if (utmSource) {
|
||||
window.localStorage.setItem('utm_source', utmSource);
|
||||
}
|
||||
|
@ -6,18 +6,22 @@
|
||||
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||
<url>
|
||||
<loc>https://ghostfol.io</loc>
|
||||
<lastmod>2021-05-14T20:24:46+00:00</lastmod>
|
||||
<lastmod>2021-06-03T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/about</loc>
|
||||
<lastmod>2021-05-14T20:24:46+00:00</lastmod>
|
||||
<lastmod>2021-06-03T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/pricing</loc>
|
||||
<lastmod>2021-05-14T20:24:46+00:00</lastmod>
|
||||
<lastmod>2021-06-03T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/register</loc>
|
||||
<lastmod>2021-06-03T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/resources</loc>
|
||||
<lastmod>2021-05-14T20:24:46+00:00</lastmod>
|
||||
<lastmod>2021-06-03T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
</urlset>
|
||||
|
@ -4,6 +4,8 @@
|
||||
|
||||
@import '~angular-material-css-vars/main';
|
||||
|
||||
@import '~svgmap/dist/svgMap';
|
||||
|
||||
$mat-css-dark-theme-selector: '.is-dark-theme';
|
||||
$mat-css-light-theme-selector: '.is-light-theme';
|
||||
|
||||
@ -79,6 +81,14 @@ body {
|
||||
color: rgba(var(--dark-primary-text)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.svgMap-tooltip {
|
||||
background: var(--dark-background);
|
||||
|
||||
.svgMap-tooltip-content table td span {
|
||||
color: rgba(var(--light-primary-text));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -153,3 +163,15 @@ ngx-skeleton-loader {
|
||||
.no-min-width {
|
||||
min-width: unset !important;
|
||||
}
|
||||
|
||||
.svgMap-tooltip {
|
||||
border-bottom: none;
|
||||
|
||||
.svgMap-tooltip-pointer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.text-decoration-underline {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
6
libs/common/src/lib/interfaces/country.interface.ts
Normal file
6
libs/common/src/lib/interfaces/country.interface.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface Country {
|
||||
code: string;
|
||||
continent: string;
|
||||
name: string;
|
||||
weight: number;
|
||||
}
|
@ -1,12 +1,15 @@
|
||||
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { Currency } from '@prisma/client';
|
||||
|
||||
import { Country } from './country.interface';
|
||||
|
||||
export interface PortfolioPosition {
|
||||
accounts: {
|
||||
[name: string]: { current: number; original: number };
|
||||
};
|
||||
allocationCurrent: number;
|
||||
allocationInvestment: number;
|
||||
countries: Country[];
|
||||
currency: Currency;
|
||||
exchange?: string;
|
||||
grossPerformance: number;
|
||||
@ -24,4 +27,5 @@ export interface PortfolioPosition {
|
||||
symbol: string;
|
||||
type?: string;
|
||||
url?: string;
|
||||
value: number;
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { Account, Order, Platform } from '@prisma/client';
|
||||
import { Account, Order, Platform, SymbolProfile } from '@prisma/client';
|
||||
|
||||
type AccountWithPlatform = Account & { Platform?: Platform };
|
||||
|
||||
export type OrderWithAccount = Order & { Account?: AccountWithPlatform };
|
||||
export type OrderWithAccount = Order & {
|
||||
Account?: AccountWithPlatform;
|
||||
SymbolProfile?: SymbolProfile;
|
||||
};
|
||||
|
10
package.json
10
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "1.10.1",
|
||||
"version": "1.14.0",
|
||||
"homepage": "https://ghostfol.io",
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@ -18,7 +18,7 @@
|
||||
"database:format-schema": "prisma format",
|
||||
"database:generate-typings": "prisma generate",
|
||||
"database:gui": "prisma studio",
|
||||
"database:push": "prisma db push --preview-feature",
|
||||
"database:push": "prisma db push",
|
||||
"database:seed": "prisma db seed --preview-feature",
|
||||
"dep-graph": "nx dep-graph",
|
||||
"e2e": "ng e2e",
|
||||
@ -65,7 +65,7 @@
|
||||
"@nestjs/schedule": "0.4.1",
|
||||
"@nestjs/serve-static": "2.1.4",
|
||||
"@nrwl/angular": "12.0.0",
|
||||
"@prisma/client": "2.20.1",
|
||||
"@prisma/client": "2.24.1",
|
||||
"@types/lodash": "4.14.168",
|
||||
"alphavantage": "2.2.0",
|
||||
"angular-material-css-vars": "1.1.2",
|
||||
@ -79,6 +79,7 @@
|
||||
"cheerio": "1.0.0-rc.6",
|
||||
"class-transformer": "0.3.2",
|
||||
"class-validator": "0.13.1",
|
||||
"countries-list": "2.6.1",
|
||||
"countup.js": "2.0.7",
|
||||
"cryptocurrencies": "7.0.0",
|
||||
"date-fns": "2.19.0",
|
||||
@ -92,10 +93,11 @@
|
||||
"passport": "0.4.1",
|
||||
"passport-google-oauth20": "2.0.0",
|
||||
"passport-jwt": "4.0.0",
|
||||
"prisma": "2.20.1",
|
||||
"prisma": "2.24.1",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"round-to": "5.0.0",
|
||||
"rxjs": "6.6.7",
|
||||
"svgmap": "2.1.1",
|
||||
"uuid": "8.3.2",
|
||||
"yahoo-finance": "0.3.6",
|
||||
"zone.js": "0.11.4"
|
||||
|
171
prisma/migrations/20210604190809_initial_migration/migration.sql
Normal file
171
prisma/migrations/20210604190809_initial_migration/migration.sql
Normal file
@ -0,0 +1,171 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AccountType" AS ENUM ('SECURITIES');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Currency" AS ENUM ('CHF', 'EUR', 'USD', 'GBP');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "DataSource" AS ENUM ('GHOSTFOLIO', 'RAKUTEN', 'YAHOO', 'ALPHA_VANTAGE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ViewMode" AS ENUM ('DEFAULT', 'ZEN');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Provider" AS ENUM ('GOOGLE', 'ANONYMOUS');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Role" AS ENUM ('USER', 'ADMIN', 'DEMO');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Type" AS ENUM ('BUY', 'SELL');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Access" (
|
||||
"granteeUserId" TEXT NOT NULL,
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
PRIMARY KEY ("id","userId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Account" (
|
||||
"accountType" "AccountType" NOT NULL DEFAULT E'SECURITIES',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"id" TEXT NOT NULL,
|
||||
"isDefault" BOOLEAN NOT NULL DEFAULT false,
|
||||
"name" TEXT,
|
||||
"platformId" TEXT,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
|
||||
PRIMARY KEY ("id","userId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Analytics" (
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"activityCount" INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
PRIMARY KEY ("userId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "MarketData" (
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"date" TIMESTAMP(3) NOT NULL,
|
||||
"id" TEXT NOT NULL,
|
||||
"symbol" TEXT NOT NULL,
|
||||
"marketPrice" DOUBLE PRECISION NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Order" (
|
||||
"currency" "Currency" NOT NULL,
|
||||
"date" TIMESTAMP(3) NOT NULL,
|
||||
"fee" DOUBLE PRECISION NOT NULL,
|
||||
"id" TEXT NOT NULL,
|
||||
"quantity" DOUBLE PRECISION NOT NULL,
|
||||
"symbol" TEXT NOT NULL,
|
||||
"type" "Type" NOT NULL,
|
||||
"unitPrice" DOUBLE PRECISION NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"accountId" TEXT,
|
||||
"accountUserId" TEXT,
|
||||
"dataSource" "DataSource" NOT NULL DEFAULT E'YAHOO',
|
||||
|
||||
PRIMARY KEY ("id","userId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Platform" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"url" TEXT NOT NULL,
|
||||
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Property" (
|
||||
"key" TEXT NOT NULL,
|
||||
"value" TEXT NOT NULL,
|
||||
|
||||
PRIMARY KEY ("key")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Settings" (
|
||||
"currency" "Currency",
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"viewMode" "ViewMode",
|
||||
|
||||
PRIMARY KEY ("userId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Subscription" (
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"id" TEXT NOT NULL,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
|
||||
PRIMARY KEY ("id","userId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL,
|
||||
"provider" "Provider",
|
||||
"thirdPartyId" TEXT,
|
||||
"accessToken" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"role" "Role" NOT NULL DEFAULT E'USER',
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"alias" TEXT,
|
||||
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "MarketData.date_symbol_unique" ON "MarketData"("date", "symbol");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "MarketData.symbol_index" ON "MarketData"("symbol");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Platform.url_unique" ON "Platform"("url");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Access" ADD FOREIGN KEY ("granteeUserId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Access" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Account" ADD FOREIGN KEY ("platformId") REFERENCES "Platform"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Account" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Analytics" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Order" ADD FOREIGN KEY ("accountId", "accountUserId") REFERENCES "Account"("id", "userId") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Order" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Settings" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Subscription" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -0,0 +1,21 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Order" ADD COLUMN "symbolProfileId" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SymbolProfile" (
|
||||
"countries" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"dataSource" "DataSource" NOT NULL,
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"symbol" TEXT NOT NULL,
|
||||
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "SymbolProfile.dataSource_symbol_unique" ON "SymbolProfile"("dataSource", "symbol");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Order" ADD FOREIGN KEY ("symbolProfileId") REFERENCES "SymbolProfile"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
@ -59,22 +59,24 @@ model MarketData {
|
||||
}
|
||||
|
||||
model Order {
|
||||
Account Account? @relation(fields: [accountId, accountUserId], references: [id, userId])
|
||||
accountId String?
|
||||
accountUserId String?
|
||||
createdAt DateTime @default(now())
|
||||
currency Currency
|
||||
dataSource DataSource @default(YAHOO)
|
||||
date DateTime
|
||||
fee Float
|
||||
id String @default(uuid())
|
||||
quantity Float
|
||||
symbol String
|
||||
type Type
|
||||
unitPrice Float
|
||||
updatedAt DateTime @updatedAt
|
||||
User User @relation(fields: [userId], references: [id])
|
||||
userId String
|
||||
Account Account? @relation(fields: [accountId, accountUserId], references: [id, userId])
|
||||
accountId String?
|
||||
accountUserId String?
|
||||
createdAt DateTime @default(now())
|
||||
currency Currency
|
||||
dataSource DataSource @default(YAHOO)
|
||||
date DateTime
|
||||
fee Float
|
||||
id String @default(uuid())
|
||||
quantity Float
|
||||
symbol String
|
||||
SymbolProfile SymbolProfile? @relation(fields: [symbolProfileId], references: [id])
|
||||
symbolProfileId String?
|
||||
type Type
|
||||
unitPrice Float
|
||||
updatedAt DateTime @updatedAt
|
||||
User User @relation(fields: [userId], references: [id])
|
||||
userId String
|
||||
|
||||
@@id([id, userId])
|
||||
}
|
||||
@ -99,6 +101,19 @@ model Settings {
|
||||
userId String @id
|
||||
}
|
||||
|
||||
model SymbolProfile {
|
||||
countries Json?
|
||||
createdAt DateTime @default(now())
|
||||
dataSource DataSource
|
||||
id String @id @default(uuid())
|
||||
name String?
|
||||
Order Order[]
|
||||
updatedAt DateTime @updatedAt
|
||||
symbol String
|
||||
|
||||
@@unique([dataSource, symbol])
|
||||
}
|
||||
|
||||
model Subscription {
|
||||
createdAt DateTime @default(now())
|
||||
expiresAt DateTime
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {
|
||||
AccountType,
|
||||
Currency,
|
||||
DataSource,
|
||||
PrismaClient,
|
||||
Role,
|
||||
Type
|
||||
@ -135,17 +136,53 @@ async function main() {
|
||||
where: { id: '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f' }
|
||||
});
|
||||
|
||||
await prisma.symbolProfile.createMany({
|
||||
data: [
|
||||
{
|
||||
countries: [{ code: 'US', weight: 1 }],
|
||||
dataSource: DataSource.YAHOO,
|
||||
id: '2bd26362-136e-411c-b578-334084b4cdcc',
|
||||
symbol: 'AMZN'
|
||||
},
|
||||
{
|
||||
countries: null,
|
||||
dataSource: DataSource.YAHOO,
|
||||
id: 'fdc42ea6-1321-44f5-9fb0-d7f1f2cf9b1e',
|
||||
symbol: 'BTCUSD'
|
||||
},
|
||||
{
|
||||
countries: [{ code: 'US', weight: 1 }],
|
||||
dataSource: DataSource.YAHOO,
|
||||
id: 'd1ee9681-fb21-4f99-a3b7-afd4fc04df2e',
|
||||
symbol: 'TSLA'
|
||||
},
|
||||
{
|
||||
countries: [
|
||||
{ code: 'US', weight: 0.9886789999999981 },
|
||||
{ code: 'NL', weight: 0.000203 },
|
||||
{ code: 'CA', weight: 0.000362 }
|
||||
],
|
||||
dataSource: DataSource.YAHOO,
|
||||
id: '7d9c8540-061e-4e7e-b019-0d0f4a84e796',
|
||||
symbol: 'VTI'
|
||||
}
|
||||
],
|
||||
skipDuplicates: true
|
||||
});
|
||||
|
||||
await prisma.order.createMany({
|
||||
data: [
|
||||
{
|
||||
accountId: '65cfb79d-b6c7-4591-9d46-73426bc62094',
|
||||
accountUserId: userDemo.id,
|
||||
currency: Currency.USD,
|
||||
dataSource: DataSource.YAHOO,
|
||||
date: new Date(Date.UTC(2017, 0, 3, 0, 0, 0)),
|
||||
fee: 30,
|
||||
id: 'cf7c0418-8535-4089-ae3d-5dbfa0aec2e1',
|
||||
quantity: 50,
|
||||
symbol: 'TSLA',
|
||||
symbolProfileId: 'd1ee9681-fb21-4f99-a3b7-afd4fc04df2e',
|
||||
type: Type.BUY,
|
||||
unitPrice: 42.97,
|
||||
userId: userDemo.id
|
||||
@ -154,11 +191,13 @@ async function main() {
|
||||
accountId: 'd804de69-0429-42dc-b6ca-b308fd7dd926',
|
||||
accountUserId: userDemo.id,
|
||||
currency: Currency.USD,
|
||||
dataSource: DataSource.YAHOO,
|
||||
date: new Date(Date.UTC(2017, 7, 16, 0, 0, 0)),
|
||||
fee: 29.9,
|
||||
id: 'a1c5d73a-8631-44e5-ac44-356827a5212c',
|
||||
quantity: 0.5614682,
|
||||
symbol: 'BTCUSD',
|
||||
symbolProfileId: 'fdc42ea6-1321-44f5-9fb0-d7f1f2cf9b1e',
|
||||
type: Type.BUY,
|
||||
unitPrice: 3562.089535970158,
|
||||
userId: userDemo.id
|
||||
@ -167,11 +206,13 @@ async function main() {
|
||||
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
||||
accountUserId: userDemo.id,
|
||||
currency: Currency.USD,
|
||||
dataSource: DataSource.YAHOO,
|
||||
date: new Date(Date.UTC(2018, 9, 1, 0, 0, 0)),
|
||||
fee: 80.79,
|
||||
id: '71c08e2a-4a86-44ae-a890-c337de5d5f9b',
|
||||
quantity: 5,
|
||||
symbol: 'AMZN',
|
||||
symbolProfileId: '2bd26362-136e-411c-b578-334084b4cdcc',
|
||||
type: Type.BUY,
|
||||
unitPrice: 2021.99,
|
||||
userId: userDemo.id
|
||||
@ -180,11 +221,13 @@ async function main() {
|
||||
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
||||
accountUserId: userDemo.id,
|
||||
currency: Currency.USD,
|
||||
dataSource: DataSource.YAHOO,
|
||||
date: new Date(Date.UTC(2019, 2, 1, 0, 0, 0)),
|
||||
fee: 19.9,
|
||||
id: '385f2c2c-d53e-4937-b0e5-e92ef6020d4e',
|
||||
quantity: 10,
|
||||
symbol: 'VTI',
|
||||
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796',
|
||||
type: Type.BUY,
|
||||
unitPrice: 144.38,
|
||||
userId: userDemo.id
|
||||
@ -193,11 +236,13 @@ async function main() {
|
||||
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
||||
accountUserId: userDemo.id,
|
||||
currency: Currency.USD,
|
||||
dataSource: DataSource.YAHOO,
|
||||
date: new Date(Date.UTC(2019, 8, 3, 0, 0, 0)),
|
||||
fee: 19.9,
|
||||
id: '185f2c2c-d53e-4937-b0e5-a93ef6020d4e',
|
||||
quantity: 10,
|
||||
symbol: 'VTI',
|
||||
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796',
|
||||
type: Type.BUY,
|
||||
unitPrice: 147.99,
|
||||
userId: userDemo.id
|
||||
@ -206,11 +251,13 @@ async function main() {
|
||||
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
||||
accountUserId: userDemo.id,
|
||||
currency: Currency.USD,
|
||||
dataSource: DataSource.YAHOO,
|
||||
date: new Date(Date.UTC(2020, 2, 2, 0, 0, 0)),
|
||||
fee: 19.9,
|
||||
id: '347b0430-a84f-4031-a0f9-390399066ad6',
|
||||
quantity: 10,
|
||||
symbol: 'VTI',
|
||||
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796',
|
||||
type: Type.BUY,
|
||||
unitPrice: 151.41,
|
||||
userId: userDemo.id
|
||||
@ -219,11 +266,13 @@ async function main() {
|
||||
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
||||
accountUserId: userDemo.id,
|
||||
currency: Currency.USD,
|
||||
dataSource: DataSource.YAHOO,
|
||||
date: new Date(Date.UTC(2020, 8, 1, 0, 0, 0)),
|
||||
fee: 19.9,
|
||||
id: '67ec3f47-3189-4b63-ba05-60d3a06b302f',
|
||||
quantity: 10,
|
||||
symbol: 'VTI',
|
||||
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796',
|
||||
type: Type.BUY,
|
||||
unitPrice: 177.69,
|
||||
userId: userDemo.id
|
||||
@ -232,11 +281,13 @@ async function main() {
|
||||
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
||||
accountUserId: userDemo.id,
|
||||
currency: Currency.USD,
|
||||
dataSource: DataSource.YAHOO,
|
||||
date: new Date(Date.UTC(2020, 2, 1, 0, 0, 0)),
|
||||
fee: 19.9,
|
||||
id: 'd01c6fbc-fa8d-47e6-8e80-66f882d2bfd2',
|
||||
quantity: 10,
|
||||
symbol: 'VTI',
|
||||
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796',
|
||||
type: Type.BUY,
|
||||
unitPrice: 203.15,
|
||||
userId: userDemo.id
|
||||
|
53
yarn.lock
53
yarn.lock
@ -2081,22 +2081,22 @@
|
||||
consola "^2.15.0"
|
||||
node-fetch "^2.6.1"
|
||||
|
||||
"@prisma/client@2.20.1":
|
||||
version "2.20.1"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-2.20.1.tgz#45e7bade3a1b58972fc35e511578e313ead18770"
|
||||
integrity sha512-/IYPubBi55rNMHfE0wwglA6eTWEZD77oz+x+3Mm9ji2lDKdS1lnYKZ0wZX0E3AB8gTNL/zsGtfzmfjgn3ePyIw==
|
||||
"@prisma/client@2.24.1":
|
||||
version "2.24.1"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-2.24.1.tgz#c4f26fb4d768dd52dd20a17e626f10e69cc0b85c"
|
||||
integrity sha512-vllhf36g3oI98GF1Q5IPmnR5MYzBPeCcl/Xiz6EAi4DMOxE069o9ka5BAqYbUG2USx8JuKw09QdMnDrp3Kyn8g==
|
||||
dependencies:
|
||||
"@prisma/engines-version" "2.20.0-26.60ba6551f29b17d7d6ce479e5733c70d9c00860e"
|
||||
"@prisma/engines-version" "2.24.1-2.18095475d5ee64536e2f93995e48ad800737a9e4"
|
||||
|
||||
"@prisma/engines-version@2.20.0-26.60ba6551f29b17d7d6ce479e5733c70d9c00860e":
|
||||
version "2.20.0-26.60ba6551f29b17d7d6ce479e5733c70d9c00860e"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.20.0-26.60ba6551f29b17d7d6ce479e5733c70d9c00860e.tgz#1189f0a7e682f500015446cfe2b34d2753452190"
|
||||
integrity sha512-fJhbGZXm2SPs/RsI79Ew4SFe+6QmChNdgU2I/SIjmU18bUgK8f1TBEWnVtFdBqEDHYPGxbpaianF7lp04KN7EA==
|
||||
"@prisma/engines-version@2.24.1-2.18095475d5ee64536e2f93995e48ad800737a9e4":
|
||||
version "2.24.1-2.18095475d5ee64536e2f93995e48ad800737a9e4"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.24.1-2.18095475d5ee64536e2f93995e48ad800737a9e4.tgz#2c5813ef98bcbe659b18b521f002f5c8aabbaae2"
|
||||
integrity sha512-60Do+ByVfHnhJ2id5h/lXOZnDQNIf5pz3enkKWOmyr744Z2IxkBu65jRckFfMN5cPtmXDre/Ay/GKm0aoeLwrw==
|
||||
|
||||
"@prisma/engines@2.20.0-26.60ba6551f29b17d7d6ce479e5733c70d9c00860e":
|
||||
version "2.20.0-26.60ba6551f29b17d7d6ce479e5733c70d9c00860e"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.20.0-26.60ba6551f29b17d7d6ce479e5733c70d9c00860e.tgz#18f23c4a8a335a93fd338f88c310f5cf120f5591"
|
||||
integrity sha512-zOWETm7DTRvlwf/CekPNSeJe6EC5bn2IFexd74wM9zgBXCZo+1sMDuNGtCqIt4Rzv8CcimEgyzrEFVq0LPV8qg==
|
||||
"@prisma/engines@2.24.1-2.18095475d5ee64536e2f93995e48ad800737a9e4":
|
||||
version "2.24.1-2.18095475d5ee64536e2f93995e48ad800737a9e4"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.24.1-2.18095475d5ee64536e2f93995e48ad800737a9e4.tgz#7e542d510f0c03f41b73edbb17254f5a0b272a4d"
|
||||
integrity sha512-29/xO9kqeQka+wN5Ev10l5L4XQXNVXdPToJs1M29VZ2imQsNsL4rtz26m3qGM54IoGWwwfTVdvuVRxKnDl2rig==
|
||||
|
||||
"@samverschueren/stream-to-observable@^0.3.0":
|
||||
version "0.3.1"
|
||||
@ -4698,6 +4698,11 @@ cosmiconfig@^7.0.0:
|
||||
path-type "^4.0.0"
|
||||
yaml "^1.10.0"
|
||||
|
||||
countries-list@2.6.1:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/countries-list/-/countries-list-2.6.1.tgz#d479757ac873b1e596ccea0a925962d20396c0cb"
|
||||
integrity sha512-jXM1Nv3U56dPQ1DsUSsEaGmLHburo4fnB7m+1yhWDUVvx5gXCd1ok/y3gXCjXzhqyawG+igcPYcAl4qjkvopaQ==
|
||||
|
||||
countup.js@2.0.7:
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/countup.js/-/countup.js-2.0.7.tgz#56b72a87fc0ee3cadb38356c246ccac88fb0a8cc"
|
||||
@ -10702,12 +10707,12 @@ pretty-format@26.x, pretty-format@^26.0.0, pretty-format@^26.6.2:
|
||||
ansi-styles "^4.0.0"
|
||||
react-is "^17.0.1"
|
||||
|
||||
prisma@2.20.1:
|
||||
version "2.20.1"
|
||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-2.20.1.tgz#28c52135523e0853258cd4ca7e883e9e4b5a9d40"
|
||||
integrity sha512-zyPvJSUfJrmciP2D/4aUrsyIefiH8AIJUeuq1a0X1df1AFw9QQ+ata/7VQdoP+RIQHnCb6Kln9kqfUw/fieljw==
|
||||
prisma@2.24.1:
|
||||
version "2.24.1"
|
||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-2.24.1.tgz#f8f4cb8baf407a71800256160277f69603bd43a3"
|
||||
integrity sha512-L+ykMpttbWzpTNsy+PPynnEX/mS1s5zs49euXBrMjxXh1M6/f9MYlTNAj+iP90O9ZSaURSpNa+1jzatPghqUcQ==
|
||||
dependencies:
|
||||
"@prisma/engines" "2.20.0-26.60ba6551f29b17d7d6ce479e5733c70d9c00860e"
|
||||
"@prisma/engines" "2.24.1-2.18095475d5ee64536e2f93995e48ad800737a9e4"
|
||||
|
||||
prismjs@^1.23.0:
|
||||
version "1.23.0"
|
||||
@ -12377,6 +12382,18 @@ supports-hyperlinks@^2.0.0:
|
||||
has-flag "^4.0.0"
|
||||
supports-color "^7.0.0"
|
||||
|
||||
svg-pan-zoom@^3.6.1:
|
||||
version "3.6.1"
|
||||
resolved "https://registry.yarnpkg.com/svg-pan-zoom/-/svg-pan-zoom-3.6.1.tgz#f880a1bb32d18e9c625d7715350bebc269b450cf"
|
||||
integrity sha512-JaKkGHHfGvRrcMPdJWkssLBeWqM+Isg/a09H7kgNNajT1cX5AztDTNs+C8UzpCxjCTRrG34WbquwaovZbmSk9g==
|
||||
|
||||
svgmap@2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/svgmap/-/svgmap-2.1.1.tgz#355c259cf4e04b20d2d39bab05d0e718ade942ff"
|
||||
integrity sha512-1blZYMYDXq8H3xykzgBJRh5q+XPd5JLOJ8K7UuZI6ab2D3hngiVcr+Z1olfy7DH9Xf9AOCTpt4Id7iVD8cKD0A==
|
||||
dependencies:
|
||||
svg-pan-zoom "^3.6.1"
|
||||
|
||||
svgo@^1.0.0:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167"
|
||||
|
Reference in New Issue
Block a user