Browse Source

Task/improve type safety in product page component (#7099)

Improve type safety
pull/6901/merge
Kenrick Tandrian 6 days ago
committed by GitHub
parent
commit
10c382a599
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 127
      apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
  2. 116
      apps/client/src/app/pages/resources/personal-finance-tools/product-page.html

127
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts

@ -5,84 +5,92 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes';
import { translate } from '@ghostfolio/ui/i18n';
import { DataService } from '@ghostfolio/ui/services';
import { Component, OnInit } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
computed,
inject
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { ActivatedRoute, RouterModule } from '@angular/router';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page' },
imports: [MatButtonModule, RouterModule],
selector: 'gf-product-page',
styleUrls: ['./product-page.scss'],
templateUrl: './product-page.html'
})
export class GfProductPageComponent implements OnInit {
public key: string;
public price: number;
public product1: Product;
public product2: Product;
public routerLinkAbout = publicRoutes.about.routerLink;
public routerLinkFeatures = publicRoutes.features.routerLink;
public routerLinkResourcesPersonalFinanceTools =
publicRoutes.resources.subRoutes.personalFinanceTools.routerLink;
public tags: string[];
public constructor(
private dataService: DataService,
private route: ActivatedRoute
) {}
public ngOnInit() {
export class GfProductPageComponent {
protected readonly price = computed(() => {
const { subscriptionOffer } = this.dataService.fetchInfo();
return subscriptionOffer?.price;
});
this.price = subscriptionOffer?.price;
protected readonly product1 = computed<Product>(() => ({
founded: 2021,
hasFreePlan: true,
hasSelfHostingAbility: true,
isOpenSource: true,
key: 'ghostfolio',
languages: [
'Chinese (简体中文)',
'Deutsch',
'English',
'Español',
'Français',
'Italiano',
'Korean (한국어)',
'Nederlands',
'Português',
'Türkçe'
],
name: 'Ghostfolio',
origin: getCountryName({ code: 'CH' }),
regions: [$localize`Global`],
slogan: 'Open Source Wealth Management',
useAnonymously: true
}));
this.product1 = {
founded: 2021,
hasFreePlan: true,
hasSelfHostingAbility: true,
isOpenSource: true,
key: 'ghostfolio',
languages: [
'Chinese (简体中文)',
'Deutsch',
'English',
'Español',
'Français',
'Italiano',
'Korean (한국어)',
'Nederlands',
'Português',
'Türkçe'
],
name: 'Ghostfolio',
origin: getCountryName({ code: 'CH' }),
regions: [$localize`Global`],
slogan: 'Open Source Wealth Management',
useAnonymously: true
};
protected readonly product2 = computed<Product>(() => {
const product = personalFinanceTools.find(({ key }) => {
return key === this.route.snapshot.data['key'];
});
this.product2 = {
...personalFinanceTools.find(({ key }) => {
return key === this.route.snapshot.data['key'];
})
const mappedProduct = {
key: product?.key ?? '',
name: product?.name ?? '',
...product
};
if (this.product2.origin) {
this.product2.origin = getCountryName({ code: this.product2.origin });
if (mappedProduct.origin) {
mappedProduct.origin = getCountryName({ code: mappedProduct.origin });
}
if (this.product2.regions) {
this.product2.regions = this.product2.regions.map((region) => {
if (mappedProduct.regions) {
mappedProduct.regions = mappedProduct.regions.map((region) => {
return translate(region);
});
}
this.tags = [
this.product1.name,
this.product1.origin,
this.product2.name,
this.product2.origin,
return mappedProduct;
});
protected readonly routerLinkAbout = publicRoutes.about.routerLink;
protected readonly routerLinkFeatures = publicRoutes.features.routerLink;
protected readonly routerLinkResourcesPersonalFinanceTools =
publicRoutes.resources.subRoutes.personalFinanceTools.routerLink;
protected readonly tags = computed<string[]>(() => {
const product1 = this.product1();
const product2 = this.product2();
return [
product1.name,
product1.origin,
product2.name,
product2.origin,
$localize`Alternative`,
$localize`App`,
$localize`Budgeting`,
@ -103,11 +111,14 @@ export class GfProductPageComponent implements OnInit {
$localize`Wealth Management`,
`WealthTech`
]
.filter((item) => {
.filter((item): item is string => {
return !!item;
})
.sort((a, b) => {
return a.localeCompare(b, undefined, { sensitivity: 'base' });
});
}
});
private readonly dataService = inject(DataService);
private readonly route = inject(ActivatedRoute);
}

116
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html

@ -6,10 +6,10 @@
<h1 class="mb-1">
<strong>Ghostfolio</strong>:
<ng-container i18n>The Open Source Alternative to</ng-container
>&nbsp;<strong>{{ product2.name }}</strong>
>&nbsp;<strong>{{ product2().name }}</strong>
</h1>
</div>
@if (product2.isArchived) {
@if (product2().isArchived) {
<div class="info-container my-4 p-2 rounded text-center text-muted">
<ng-container i18n>This page has been archived.</ng-container>
</div>
@ -17,7 +17,7 @@
<section class="mb-4">
<p i18n>
Are you looking for an open source alternative to
{{ product2.name }}?
{{ product2().name }}?
<a [routerLink]="routerLinkAbout">Ghostfolio</a> is a powerful
portfolio management tool that provides individuals with a
comprehensive platform to track, analyze, and optimize their
@ -31,7 +31,7 @@
</p>
<p i18n>
Ghostfolio is an open source software (OSS), providing a
cost-effective alternative to {{ product2.name }} making it
cost-effective alternative to {{ product2().name }} making it
particularly suitable for individuals on a tight budget, such as
those
<a href="../en/blog/2023/07/exploring-the-path-to-fire"
@ -42,9 +42,9 @@
</p>
<p i18n>
Let’s dive deeper into the detailed Ghostfolio vs
{{ product2.name }} comparison table below to gain a thorough
{{ product2().name }} comparison table below to gain a thorough
understanding of how Ghostfolio positions itself relative to
{{ product2.name }}. We will explore various aspects such as
{{ product2().name }}. We will explore various aspects such as
features, data privacy, pricing, and more, allowing you to make a
well-informed choice for your personal requirements.
</p>
@ -54,7 +54,7 @@
<caption class="text-center" i18n>
Ghostfolio vs
{{
product2.name
product2().name
}}
comparison table
</caption>
@ -63,31 +63,31 @@
<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 }}
{{ product2().name }}
</th>
</tr>
</thead>
<tbody>
<tr class="mat-mdc-row">
<td class="mat-mdc-cell px-3 py-2 text-right"></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>
<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-3 py-2 text-right" 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>
<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-3 py-2 text-right" 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>
<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-3 py-2 text-right" i18n>Region</td>
<td class="mat-mdc-cell px-1 py-2">
@for (
region of product1.regions;
region of product1().regions;
track region;
let isLast = $last
) {
@ -96,7 +96,7 @@
</td>
<td class="mat-mdc-cell px-1 py-2">
@for (
region of product2.regions;
region of product2().regions;
track region;
let isLast = $last
) {
@ -110,7 +110,7 @@
</td>
<td class="mat-mdc-cell px-1 py-2">
@for (
language of product1.languages;
language of product1().languages;
track language;
let isLast = $last
) {
@ -119,7 +119,7 @@
</td>
<td class="mat-mdc-cell px-1 py-2">
@for (
language of product2.languages;
language of product2().languages;
track language;
let isLast = $last
) {
@ -132,35 +132,35 @@
Open Source Software
</td>
<td class="mat-mdc-cell px-1 py-2">
@if (product1.isOpenSource) {
@if (product1().isOpenSource) {
<span
i18n
i18n-title
title="{{ product1.name }} is Open Source Software"
title="{{ product1().name }} is Open Source Software"
>✅ Yes</span
>
} @else {
<span
i18n
i18n-title
title="{{ product1.name }} is not Open Source Software"
title="{{ product1().name }} is not Open Source Software"
>❌ No</span
>
}
</td>
<td class="mat-mdc-cell px-1 py-2">
@if (product2.isOpenSource) {
@if (product2().isOpenSource) {
<span
i18n
i18n-title
title="{{ product2.name }} is Open Source Software"
title="{{ product2().name }} is Open Source Software"
>✅ Yes</span
>
} @else {
<span
i18n
i18n-title
title="{{ product2.name }} is not Open Source Software"
title="{{ product2().name }} is not Open Source Software"
>❌ No</span
>
}
@ -171,35 +171,35 @@
Self-Hosting
</td>
<td class="mat-mdc-cell px-1 py-2">
@if (product1.hasSelfHostingAbility === true) {
@if (product1().hasSelfHostingAbility === true) {
<span
i18n
i18n-title
title="{{ product1.name }} can be self-hosted"
title="{{ product1().name }} can be self-hosted"
>✅ Yes</span
>
} @else if (product1.hasSelfHostingAbility === false) {
} @else if (product1().hasSelfHostingAbility === false) {
<span
i18n
i18n-title
title="{{ product1.name }} cannot be self-hosted"
title="{{ product1().name }} cannot be self-hosted"
>❌ No</span
>
}
</td>
<td class="mat-mdc-cell px-1 py-2">
@if (product2.hasSelfHostingAbility === true) {
@if (product2().hasSelfHostingAbility === true) {
<span
i18n
i18n-title
title="{{ product2.name }} can be self-hosted"
title="{{ product2().name }} can be self-hosted"
>✅ Yes</span
>
} @else if (product2.hasSelfHostingAbility === false) {
} @else if (product2().hasSelfHostingAbility === false) {
<span
i18n
i18n-title
title="{{ product2.name }} cannot be self-hosted"
title="{{ product2().name }} cannot be self-hosted"
>❌ No</span
>
}
@ -210,35 +210,35 @@
Use anonymously
</td>
<td class="mat-mdc-cell px-1 py-2">
@if (product1.useAnonymously === true) {
@if (product1().useAnonymously === true) {
<span
i18n
i18n-title
title="{{ product1.name }} can be used anonymously"
title="{{ product1().name }} can be used anonymously"
>✅ Yes</span
>
} @else if (product1.useAnonymously === false) {
} @else if (product1().useAnonymously === false) {
<span
i18n
i18n-title
title="{{ product1.name }} cannot be used anonymously"
title="{{ product1().name }} cannot be used anonymously"
>❌ No</span
>
}
</td>
<td class="mat-mdc-cell px-1 py-2">
@if (product2.useAnonymously === true) {
@if (product2().useAnonymously === true) {
<span
i18n
i18n-title
title="{{ product2.name }} can be used anonymously"
title="{{ product2().name }} can be used anonymously"
>✅ Yes</span
>
} @else if (product2.useAnonymously === false) {
} @else if (product2().useAnonymously === false) {
<span
i18n
i18n-title
title="{{ product2.name }} cannot be used anonymously"
title="{{ product2().name }} cannot be used anonymously"
>❌ No</span
>
}
@ -249,35 +249,35 @@
Free Plan
</td>
<td class="mat-mdc-cell px-1 py-2">
@if (product1.hasFreePlan === true) {
@if (product1().hasFreePlan === true) {
<span
i18n
i18n-title
title="{{ product1.name }} offers a free plan"
title="{{ product1().name }} offers a free plan"
>✅ Yes</span
>
} @else if (product1.hasFreePlan === false) {
} @else if (product1().hasFreePlan === false) {
<span
i18n
i18n-title
title="{{ product1.name }} does not offer a free plan"
title="{{ product1().name }} does not offer a free plan"
>❌ No</span
>
}
</td>
<td class="mat-mdc-cell px-1 py-2">
@if (product2.hasFreePlan === true) {
@if (product2().hasFreePlan === true) {
<span
i18n
i18n-title
title="{{ product2.name }} offers a free plan"
title="{{ product2().name }} offers a free plan"
>✅ Yes</span
>
} @else if (product2.hasFreePlan === false) {
} @else if (product2().hasFreePlan === false) {
<span
i18n
i18n-title
title="{{ product2.name }} does not offer a free plan"
title="{{ product2().name }} does not offer a free plan"
>❌ No</span
>
}
@ -286,22 +286,22 @@
<tr class="mat-mdc-row">
<td class="mat-mdc-cell px-3 py-2 text-right" i18n>Pricing</td>
<td class="mat-mdc-cell px-1 py-2">
<span i18n>Starting from</span> ${{ price }} /
<span i18n>Starting from</span> ${{ price() }} /
<span i18n>year</span>
</td>
<td class="mat-mdc-cell px-1 py-2">
@if (product2.pricingPerYear) {
@if (product2().pricingPerYear) {
<span i18n>Starting from</span>
{{ product2.pricingPerYear }} /
{{ product2().pricingPerYear }} /
<span i18n>year</span>
}
</td>
</tr>
@if (product1.note || product2.note) {
@if (product1().note || product2().note) {
<tr class="mat-mdc-row">
<td class="mat-mdc-cell px-3 py-2 text-right" 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>
<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>
@ -310,9 +310,9 @@
<section class="mb-4">
<p i18n>
Please note that the information provided in the Ghostfolio vs
{{ product2.name }} comparison table is based on our independent
{{ product2().name }} comparison table is based on our independent
research and analysis. This website is not affiliated with
{{ product2.name }} or any other product mentioned in the
{{ product2().name }} or any other product mentioned in the
comparison. As the landscape of personal finance tools evolves, it
is essential to verify any specific details or changes directly from
the respective product page. Data needs a refresh? Help us maintain
@ -337,7 +337,7 @@
</section>
<section class="mb-4">
<ul class="list-inline">
@for (tag of tags; track tag) {
@for (tag of tags(); track tag) {
<li class="list-inline-item">
<span class="badge badge-light">{{ tag }}</span>
</li>
@ -355,7 +355,7 @@
aria-current="page"
class="active breadcrumb-item text-truncate"
>
Ghostfolio vs {{ product2.name }}
Ghostfolio vs {{ product2().name }}
</li>
</ol>
</nav>

Loading…
Cancel
Save