mirror of https://github.com/ghostfolio/ghostfolio
14 changed files with 426 additions and 1 deletions
@ -0,0 +1,48 @@ |
|||
import { NgModule } from '@angular/core'; |
|||
import { RouterModule, Routes } from '@angular/router'; |
|||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; |
|||
|
|||
import { AlternativesPageComponent } from './alternatives-page.component'; |
|||
import { data } from './data'; |
|||
|
|||
const routes: Routes = [ |
|||
{ |
|||
canActivate: [AuthGuard], |
|||
component: AlternativesPageComponent, |
|||
path: '', |
|||
title: $localize`Alternatives` |
|||
}, |
|||
{ |
|||
canActivate: [AuthGuard], |
|||
path: 'maybe', |
|||
loadComponent: () => |
|||
import('./products/maybe-page.component').then( |
|||
(c) => c.MaybePageComponent |
|||
), |
|||
title: data.find(({ key }) => key === 'maybe').name |
|||
}, |
|||
{ |
|||
canActivate: [AuthGuard], |
|||
path: 'parqet', |
|||
loadComponent: () => |
|||
import('./products/parqet-page.component').then( |
|||
(c) => c.ParqetPageComponent |
|||
), |
|||
title: data.find(({ key }) => key === 'parqet').name |
|||
}, |
|||
{ |
|||
canActivate: [AuthGuard], |
|||
path: 'yeekatee', |
|||
loadComponent: () => |
|||
import('./products/yeekatee-page.component').then( |
|||
(c) => c.YeekateePageComponent |
|||
), |
|||
title: data.find(({ key }) => key === 'yeekatee').name |
|||
} |
|||
]; |
|||
|
|||
@NgModule({ |
|||
imports: [RouterModule.forChild(routes)], |
|||
exports: [RouterModule] |
|||
}) |
|||
export class AlternativesPageRoutingModule {} |
@ -0,0 +1,25 @@ |
|||
import { Component, OnDestroy } from '@angular/core'; |
|||
import { Subject } from 'rxjs'; |
|||
|
|||
import { data } from './data'; |
|||
|
|||
@Component({ |
|||
host: { class: 'page' }, |
|||
selector: 'gf-alternatives-page', |
|||
styleUrls: ['./alternatives-page.scss'], |
|||
templateUrl: './alternatives-page.html' |
|||
}) |
|||
export class AlternativesPageComponent implements OnDestroy { |
|||
public products = data.filter(({ key }) => { |
|||
return key !== 'ghostfolio'; |
|||
}); |
|||
|
|||
private unsubscribeSubject = new Subject<void>(); |
|||
|
|||
public constructor() {} |
|||
|
|||
public ngOnDestroy() { |
|||
this.unsubscribeSubject.next(); |
|||
this.unsubscribeSubject.complete(); |
|||
} |
|||
} |
@ -0,0 +1,37 @@ |
|||
<div class="container"> |
|||
<div class="mb-5 row"> |
|||
<div class="col"> |
|||
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Alternatives</h3> |
|||
<mat-card |
|||
*ngFor="let product of products" |
|||
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" |
|||
[routerLink]="['/alternatives', product.key]" |
|||
> |
|||
<div class="flex-grow-1 overflow-hidden"> |
|||
<div class="h6 m-0 text-truncate"> |
|||
Ghostfolio: The open source alternative to {{ product.name |
|||
}} |
|||
</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> |
|||
</div> |
|||
</div> |
|||
</div> |
@ -0,0 +1,13 @@ |
|||
import { CommonModule } from '@angular/common'; |
|||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; |
|||
import { MatCardModule } from '@angular/material/card'; |
|||
|
|||
import { AlternativesPageRoutingModule } from './alternatives-page-routing.module'; |
|||
import { AlternativesPageComponent } from './alternatives-page.component'; |
|||
|
|||
@NgModule({ |
|||
declarations: [AlternativesPageComponent], |
|||
imports: [AlternativesPageRoutingModule, CommonModule, MatCardModule], |
|||
schemas: [CUSTOM_ELEMENTS_SCHEMA] |
|||
}) |
|||
export class AlternativesPageModule {} |
@ -0,0 +1,8 @@ |
|||
:host { |
|||
color: rgb(var(--dark-primary-text)); |
|||
display: block; |
|||
} |
|||
|
|||
:host-context(.is-dark-theme) { |
|||
color: rgb(var(--light-primary-text)); |
|||
} |
@ -0,0 +1,47 @@ |
|||
import { Comparison } from '@ghostfolio/common/interfaces'; |
|||
|
|||
export const data: Comparison[] = [ |
|||
{ |
|||
founded: 2021, |
|||
hasFreePlan: true, |
|||
isOpenSource: true, |
|||
key: 'ghostfolio', |
|||
languages: |
|||
'English, Dutch, French, German, Italian, Portuguese and Spanish', |
|||
name: 'Ghostfolio', |
|||
origin: 'Switzerland', |
|||
pricing: 'Starting from $19 / year', |
|||
region: 'Global', |
|||
slogan: 'Open Source Wealth Management' |
|||
}, |
|||
{ |
|||
founded: 2021, |
|||
isOpenSource: false, |
|||
key: 'maybe', |
|||
languages: 'English', |
|||
name: 'Maybe Finance', |
|||
note: 'Sunset in 2023', |
|||
origin: 'USA', |
|||
pricing: 'Starting from $145 / year', |
|||
region: 'USA', |
|||
slogan: 'Your financial future, in your control' |
|||
}, |
|||
{ |
|||
hasFreePlan: true, |
|||
isOpenSource: false, |
|||
key: 'parqet', |
|||
name: 'Parqet', |
|||
origin: 'Germany', |
|||
region: 'DACH', |
|||
slogan: 'Dein Vermögen immer im Blick' |
|||
}, |
|||
{ |
|||
founded: 2021, |
|||
isOpenSource: false, |
|||
key: 'yeekatee', |
|||
name: 'yeekatee', |
|||
origin: 'Switzerland', |
|||
region: 'CH', |
|||
slogan: 'Connect. Share. Invest.' |
|||
} |
|||
]; |
@ -0,0 +1,151 @@ |
|||
<div class="container"> |
|||
<div class="row"> |
|||
<div class="col-md-8 offset-md-2"> |
|||
<article> |
|||
<div class="mb-4 text-center"> |
|||
<h1 class="mb-1"> |
|||
Ghostfolio: The open source alternative to {{ product2.name }} |
|||
</h1> |
|||
</div> |
|||
<section class="mb-4"> |
|||
<p> |
|||
Are you looking for an open source alternative to {{ product2.name |
|||
}}? Compare Ghostfolio to {{ product2.name }} using the table below. |
|||
</p> |
|||
</section> |
|||
<section class="mb-4"> |
|||
<table class="gf-table w-100"> |
|||
<thead> |
|||
<tr class="mat-mdc-header-row"> |
|||
<th class="mat-mdc-header-cell px-1 py-2"></th> |
|||
<th class="mat-mdc-header-cell px-1 py-2">Ghostfolio</th> |
|||
<th class="mat-mdc-header-cell px-1 py-2"> |
|||
{{ product2.name }} |
|||
</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
<tr class="mat-mdc-row"> |
|||
<td class="mat-mdc-cell px-1 py-2"></td> |
|||
<td class="mat-mdc-cell px-1 py-2">{{ product1.slogan }}</td> |
|||
<td class="mat-mdc-cell px-1 py-2">{{ product2.slogan }}</td> |
|||
</tr> |
|||
<tr class="mat-mdc-row"> |
|||
<td class="mat-mdc-cell px-1 py-2" i18n>Founded</td> |
|||
<td class="mat-mdc-cell px-1 py-2">{{ product1.founded }}</td> |
|||
<td class="mat-mdc-cell px-1 py-2">{{ product2.founded }}</td> |
|||
</tr> |
|||
<tr class="mat-mdc-row"> |
|||
<td class="mat-mdc-cell px-1 py-2" i18n>Origin</td> |
|||
<td class="mat-mdc-cell px-1 py-2">{{ product1.origin }}</td> |
|||
<td class="mat-mdc-cell px-1 py-2">{{ product2.origin }}</td> |
|||
</tr> |
|||
<tr class="mat-mdc-row"> |
|||
<td class="mat-mdc-cell px-1 py-2" i18n>Region</td> |
|||
<td class="mat-mdc-cell px-1 py-2">{{ product1.region }}</td> |
|||
<td class="mat-mdc-cell px-1 py-2">{{ product2.region }}</td> |
|||
</tr> |
|||
<tr class="mat-mdc-row"> |
|||
<td class="mat-mdc-cell px-1 py-2" i18n>Available in</td> |
|||
<td class="mat-mdc-cell px-1 py-2">{{ product1.languages }}</td> |
|||
<td class="mat-mdc-cell px-1 py-2">{{ product2.languages }}</td> |
|||
</tr> |
|||
<tr class="mat-mdc-row"> |
|||
<td class="mat-mdc-cell px-1 py-2" i18n> |
|||
Open Source Software |
|||
</td> |
|||
<td class="mat-mdc-cell px-1 py-2"> |
|||
<ng-container *ngIf="product1.isOpenSource === true" |
|||
>✅</ng-container |
|||
><ng-container *ngIf="product1.isOpenSource === false" |
|||
>❌</ng-container |
|||
> |
|||
</td> |
|||
<td class="mat-mdc-cell px-1 py-2"> |
|||
<ng-container *ngIf="product2.isOpenSource === true" |
|||
>✅</ng-container |
|||
><ng-container *ngIf="product2.isOpenSource === false" |
|||
>❌</ng-container |
|||
> |
|||
</td> |
|||
</tr> |
|||
<tr class="mat-mdc-row"> |
|||
<td class="mat-mdc-cell px-1 py-2" i18n>Free Plan</td> |
|||
<td class="mat-mdc-cell px-1 py-2"> |
|||
<ng-container *ngIf="product1.hasFreePlan === true" |
|||
>✅</ng-container |
|||
><ng-container *ngIf="product1.hasFreePlan === false" |
|||
>❌</ng-container |
|||
> |
|||
</td> |
|||
<td class="mat-mdc-cell px-1 py-2"> |
|||
<ng-container *ngIf="product2.hasFreePlan === true" |
|||
>✅</ng-container |
|||
><ng-container *ngIf="product2.hasFreePlan === false" |
|||
>❌</ng-container |
|||
> |
|||
</td> |
|||
</tr> |
|||
<tr class="mat-mdc-row"> |
|||
<td class="mat-mdc-cell px-1 py-2" i18n>Pricing</td> |
|||
<td class="mat-mdc-cell px-1 py-2">{{ product1.pricing }}</td> |
|||
<td class="mat-mdc-cell px-1 py-2">{{ product2.pricing }}</td> |
|||
</tr> |
|||
<tr *ngIf="product1.note || product2.note" class="mat-mdc-row"> |
|||
<td class="mat-mdc-cell px-1 py-2" i18n>Notes</td> |
|||
<td class="mat-mdc-cell px-1 py-2">{{ product1.note }}</td> |
|||
<td class="mat-mdc-cell px-1 py-2">{{ product2.note }}</td> |
|||
</tr> |
|||
</tbody> |
|||
</table> |
|||
</section> |
|||
<section class="mb-4 py-3"> |
|||
<h2 class="h4 mb-0 text-center"> |
|||
Would you like to <strong>refine</strong> your |
|||
<strong>personal investment strategy</strong>? |
|||
</h2> |
|||
<p class="lead mb-2 text-center" i18n> |
|||
Ghostfolio empowers you to keep track of your wealth. |
|||
</p> |
|||
<div class="text-center"> |
|||
<a color="primary" href="https://ghostfol.io" mat-flat-button> |
|||
Get Started |
|||
</a> |
|||
</div> |
|||
</section> |
|||
<section class="mb-4"> |
|||
<ul class="list-inline"> |
|||
<li class="list-inline-item"> |
|||
<span class="badge badge-light">App</span> |
|||
</li> |
|||
<li class="list-inline-item"> |
|||
<span class="badge badge-light">{{ product2.name }}</span> |
|||
</li> |
|||
<li class="list-inline-item"> |
|||
<span class="badge badge-light">{{ product2.name }}</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">Personal Finance</span> |
|||
</li> |
|||
</ul> |
|||
</section> |
|||
<nav aria-label="breadcrumb"> |
|||
<ol class="breadcrumb"> |
|||
<li class="breadcrumb-item"> |
|||
<a i18n [routerLink]="['/alternatives']">Alternatives</a> |
|||
</li> |
|||
<li |
|||
aria-current="page" |
|||
class="active breadcrumb-item text-truncate" |
|||
> |
|||
{{ product2.name }} |
|||
</li> |
|||
</ol> |
|||
</nav> |
|||
</article> |
|||
</div> |
|||
</div> |
|||
</div> |
@ -0,0 +1,24 @@ |
|||
import { CommonModule } from '@angular/common'; |
|||
import { Component } from '@angular/core'; |
|||
import { MatButtonModule } from '@angular/material/button'; |
|||
import { RouterModule } from '@angular/router'; |
|||
import { Comparison } from '@ghostfolio/common/interfaces'; |
|||
|
|||
import { data } from '../data'; |
|||
|
|||
@Component({ |
|||
host: { class: 'page' }, |
|||
imports: [CommonModule, MatButtonModule, RouterModule], |
|||
selector: 'gf-maybe-page', |
|||
standalone: true, |
|||
templateUrl: '../page-template.html' |
|||
}) |
|||
export class MaybePageComponent { |
|||
public product1: Comparison = data.find(({ key }) => { |
|||
return key === 'ghostfolio'; |
|||
}); |
|||
|
|||
public product2: Comparison = data.find(({ key }) => { |
|||
return key === 'maybe'; |
|||
}); |
|||
} |
@ -0,0 +1,24 @@ |
|||
import { CommonModule } from '@angular/common'; |
|||
import { Component } from '@angular/core'; |
|||
import { MatButtonModule } from '@angular/material/button'; |
|||
import { RouterModule } from '@angular/router'; |
|||
import { Comparison } from '@ghostfolio/common/interfaces'; |
|||
|
|||
import { data } from '../data'; |
|||
|
|||
@Component({ |
|||
host: { class: 'page' }, |
|||
imports: [CommonModule, MatButtonModule, RouterModule], |
|||
selector: 'gf-parqet-page', |
|||
standalone: true, |
|||
templateUrl: '../page-template.html' |
|||
}) |
|||
export class ParqetPageComponent { |
|||
public product1: Comparison = data.find(({ key }) => { |
|||
return key === 'ghostfolio'; |
|||
}); |
|||
|
|||
public product2: Comparison = data.find(({ key }) => { |
|||
return key === 'parqet'; |
|||
}); |
|||
} |
@ -0,0 +1,24 @@ |
|||
import { CommonModule } from '@angular/common'; |
|||
import { Component } from '@angular/core'; |
|||
import { MatButtonModule } from '@angular/material/button'; |
|||
import { RouterModule } from '@angular/router'; |
|||
import { Comparison } from '@ghostfolio/common/interfaces'; |
|||
|
|||
import { data } from '../data'; |
|||
|
|||
@Component({ |
|||
host: { class: 'page' }, |
|||
imports: [CommonModule, MatButtonModule, RouterModule], |
|||
selector: 'gf-yeekatee-page', |
|||
standalone: true, |
|||
templateUrl: '../page-template.html' |
|||
}) |
|||
export class YeekateePageComponent { |
|||
public product1: Comparison = data.find(({ key }) => { |
|||
return key === 'ghostfolio'; |
|||
}); |
|||
|
|||
public product2: Comparison = data.find(({ key }) => { |
|||
return key === 'yeekatee'; |
|||
}); |
|||
} |
@ -0,0 +1,13 @@ |
|||
export interface Comparison { |
|||
founded?: number; |
|||
hasFreePlan?: boolean; |
|||
isOpenSource: boolean; |
|||
key: string; |
|||
languages?: string; |
|||
name: string; |
|||
note?: string; |
|||
origin?: string; |
|||
pricing?: string; |
|||
region?: string; |
|||
slogan?: string; |
|||
} |
Loading…
Reference in new issue