Browse Source

Setup comparison pages

pull/2135/head
Thomas 2 years ago
parent
commit
81099bdd39
  1. 7
      apps/client/src/app/app-routing.module.ts
  2. 4
      apps/client/src/app/app.component.html
  3. 48
      apps/client/src/app/pages/alternatives/alternatives-page-routing.module.ts
  4. 25
      apps/client/src/app/pages/alternatives/alternatives-page.component.ts
  5. 37
      apps/client/src/app/pages/alternatives/alternatives-page.html
  6. 13
      apps/client/src/app/pages/alternatives/alternatives-page.module.ts
  7. 8
      apps/client/src/app/pages/alternatives/alternatives-page.scss
  8. 47
      apps/client/src/app/pages/alternatives/data.ts
  9. 151
      apps/client/src/app/pages/alternatives/page-template.html
  10. 24
      apps/client/src/app/pages/alternatives/products/maybe-page.component.ts
  11. 24
      apps/client/src/app/pages/alternatives/products/parqet-page.component.ts
  12. 24
      apps/client/src/app/pages/alternatives/products/yeekatee-page.component.ts
  13. 13
      libs/common/src/lib/interfaces/comparison.ts
  14. 2
      libs/common/src/lib/interfaces/index.ts

7
apps/client/src/app/app-routing.module.ts

@ -37,6 +37,13 @@ const routes: Routes = [
loadChildren: () =>
import('./pages/admin/admin-page.module').then((m) => m.AdminPageModule)
},
...['alternatives'].map((path) => ({
path,
loadChildren: () =>
import('./pages/alternatives/alternatives-page.module').then(
(m) => m.AlternativesPageModule
)
})),
{
path: 'auth',
loadChildren: () =>

4
apps/client/src/app/app.component.html

@ -45,7 +45,8 @@
<footer
*ngIf="
(currentRoute === 'blog' ||
(currentRoute === 'alternatives' ||
currentRoute === 'blog' ||
currentRoute === 'faq' ||
currentRoute === 'features' ||
currentRoute === 'markets' ||
@ -70,6 +71,7 @@
<a i18n [routerLink]="['/markets']">Markets</a>
</li>
<li><a i18n [routerLink]="['/resources']">Resources</a></li>
<li><a i18n [routerLink]="['/alternatives']">Alternatives</a></li>
</ul>
</div>
<div class="col-sm">

48
apps/client/src/app/pages/alternatives/alternatives-page-routing.module.ts

@ -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 {}

25
apps/client/src/app/pages/alternatives/alternatives-page.component.ts

@ -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();
}
}

37
apps/client/src/app/pages/alternatives/alternatives-page.html

@ -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>

13
apps/client/src/app/pages/alternatives/alternatives-page.module.ts

@ -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 {}

8
apps/client/src/app/pages/alternatives/alternatives-page.scss

@ -0,0 +1,8 @@
:host {
color: rgb(var(--dark-primary-text));
display: block;
}
:host-context(.is-dark-theme) {
color: rgb(var(--light-primary-text));
}

47
apps/client/src/app/pages/alternatives/data.ts

@ -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.'
}
];

151
apps/client/src/app/pages/alternatives/page-template.html

@ -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>

24
apps/client/src/app/pages/alternatives/products/maybe-page.component.ts

@ -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';
});
}

24
apps/client/src/app/pages/alternatives/products/parqet-page.component.ts

@ -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';
});
}

24
apps/client/src/app/pages/alternatives/products/yeekatee-page.component.ts

@ -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';
});
}

13
libs/common/src/lib/interfaces/comparison.ts

@ -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;
}

2
libs/common/src/lib/interfaces/index.ts

@ -10,6 +10,7 @@ import type {
import type { BenchmarkMarketDataDetails } from './benchmark-market-data-details.interface';
import type { BenchmarkProperty } from './benchmark-property.interface';
import type { Benchmark } from './benchmark.interface';
import { Comparison } from './comparison';
import type { Coupon } from './coupon.interface';
import type { DataProviderInfo } from './data-provider-info.interface';
import type { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface';
@ -58,6 +59,7 @@ export {
BenchmarkMarketDataDetails,
BenchmarkProperty,
BenchmarkResponse,
Comparison,
Coupon,
DataProviderInfo,
EnhancedSymbolProfile,

Loading…
Cancel
Save