Feature/add statistics section to landing page (#1306)

* Add pulls on Docker Hub to statistics

* Add statistics to landing page

* Update changelog
pull/1307/head
Thomas Kaul 2 years ago committed by GitHub
parent 9562139fa6
commit 8d3954304e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -9,7 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added a mini statistics section to the landing page including pulls on _Docker Hub_
- Added an _As seen in_ section to the landing page
- Added support for an icon in the value component
### Changed

@ -145,6 +145,27 @@ export class InfoService {
});
}
private async countDockerHubPulls(): Promise<number> {
try {
const get = bent(
`https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio`,
'GET',
'json',
200,
{
'User-Agent': 'request'
}
);
const { pull_count } = await get();
return pull_count;
} catch (error) {
Logger.error(error, 'InfoService');
return undefined;
}
}
private async countGitHubContributors(): Promise<number> {
try {
const get = bent(
@ -245,6 +266,8 @@ export class InfoService {
const activeUsers1d = await this.countActiveUsers(1);
const activeUsers30d = await this.countActiveUsers(30);
const newUsers30d = await this.countNewUsers(30);
const dockerHubPulls = await this.countDockerHubPulls();
const gitHubContributors = await this.countGitHubContributors();
const gitHubStargazers = await this.countGitHubStargazers();
const slackCommunityUsers = await this.countSlackCommunityUsers();
@ -252,6 +275,7 @@ export class InfoService {
statistics = {
activeUsers1d,
activeUsers30d,
dockerHubPulls,
gitHubContributors,
gitHubStargazers,
newUsers30d,

@ -1,4 +1,7 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { DataService } from '@ghostfolio/client/services/data.service';
import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { format } from 'date-fns';
import { Subject } from 'rxjs';
@ -11,6 +14,8 @@ import { Subject } from 'rxjs';
export class LandingPageComponent implements OnDestroy, OnInit {
public currentYear = format(new Date(), 'yyyy');
public demoAuthToken: string;
public hasPermissionForStatistics: boolean;
public statistics: Statistics;
public testimonials = [
{
author: 'Philipp',
@ -36,7 +41,16 @@ export class LandingPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
public constructor() {}
public constructor(private dataService: DataService) {
const { globalPermissions, statistics } = this.dataService.fetchInfo();
this.hasPermissionForStatistics = hasPermission(
globalPermissions,
permissions.enableStatistics
);
this.statistics = statistics;
}
public ngOnInit() {}

@ -42,7 +42,52 @@
</div>
</div>
<div class="row my-3">
<div *ngIf="hasPermissionForStatistics" class="row mb-5">
<div class="col-md-4 d-flex my-1">
<a
class="d-block"
title="Ghostfolio in Numbers: Monthly Active Users (MAU)"
[routerLink]="['/about']"
>
<gf-value
icon="people-outline"
size="large"
[value]="statistics?.activeUsers30d ?? '-'"
>Monthly Active Users</gf-value
>
</a>
</div>
<div class="col-md-4 d-flex my-1">
<a
class="d-block"
title="Ghostfolio in Numbers: Stars on GitHub"
[routerLink]="['/about']"
>
<gf-value
icon="star-outline"
size="large"
[value]="statistics?.gitHubStargazers ?? '-'"
>Stars on GitHub</gf-value
>
</a>
</div>
<div class="col-md-4 d-flex my-1">
<a
class="d-block"
title="Ghostfolio in Numbers: Pulls on Docker Hub"
[routerLink]="['/about']"
>
<gf-value
icon="cloud-download-outline"
size="large"
[value]="statistics?.dockerHubPulls ?? '-'"
>Pulls on Docker Hub</gf-value
>
</a>
</div>
</div>
<div class="row mb-5">
<div class="col-12 text-center text-muted"><small>As seen in</small></div>
<div class="col-md-2 d-flex justify-content-center my-1">
<a
@ -110,20 +155,20 @@
<div class="row my-3">
<div class="col-md-4 my-2">
<mat-card>
<mat-card-title class="text-center">360° View</mat-card-title>
<mat-card-title>360° View</mat-card-title>
Get the full picture of your personal finances across multiple
platforms.
</mat-card>
</div>
<div class="col-md-4 my-2">
<mat-card>
<mat-card-title class="text-center">Web3 Ready</mat-card-title>
<mat-card-title>Web3 Ready</mat-card-title>
Use Ghostfolio anonymously and own your financial data.
</mat-card>
</div>
<div class="col-md-4 my-2">
<mat-card>
<mat-card-title class="text-center">Open Source</mat-card-title>
<mat-card-title>Open Source</mat-card-title>
Benefit from continuous improvements through a strong community.
</mat-card>
</div>

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { RouterModule } from '@angular/router';
import { GfLogoModule } from '@ghostfolio/ui/logo';
import { GfValueModule } from '@ghostfolio/ui/value';
import { LandingPageRoutingModule } from './landing-page-routing.module';
import { LandingPageComponent } from './landing-page.component';
@ -13,6 +14,7 @@ import { LandingPageComponent } from './landing-page.component';
imports: [
CommonModule,
GfLogoModule,
GfValueModule,
LandingPageRoutingModule,
MatButtonModule,
MatCardModule,

@ -18,6 +18,7 @@ $mat-css-light-theme-selector: '.is-light-theme';
:root {
--dark-background: rgb(39, 39, 39);
--font-family-sans-serif: Roboto, 'Helvetica Neue', sans-serif;
--light-background: rgb(255, 255, 255);
}
@ -146,6 +147,10 @@ ngx-skeleton-loader {
@include gf-table;
}
.lead {
font-weight: unset;
}
.mat-card {
&:not([class*='mat-elevation-z']) {
border: 1px solid rgba(var(--dark-dividers));

@ -1,6 +1,7 @@
export interface Statistics {
activeUsers1d: number;
activeUsers30d: number;
dockerHubPulls: number;
gitHubContributors: number;
gitHubStargazers: number;
newUsers30d: number;

@ -1,67 +1,73 @@
<ng-template #label><ng-content></ng-content></ng-template>
<ng-container *ngIf="value || value === 0 || value === null">
<div
class="d-flex"
[ngClass]="position === 'end' ? 'justify-content-end' : ''"
>
<ng-container *ngIf="isNumber || value === null">
<ng-container *ngIf="colorizeSign && !useAbsoluteValue">
<div *ngIf="value > 0" class="mr-1 text-success">+</div>
<div *ngIf="value < 0" class="mr-1 text-danger">-</div>
</ng-container>
<div
*ngIf="isPercent"
class="mb-0 value"
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
>
{{ formattedValue }}%
</div>
<div
*ngIf="!isPercent"
class="mb-0 value"
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
>
<ng-container *ngIf="value === null">
<span class="text-monospace text-muted">***</span>
</ng-container>
<ng-container *ngIf="value !== null">
{{ formattedValue }}
<div *ngIf="icon" class="align-self-center mr-3">
<ion-icon class="h3 m-0" [name]="icon"></ion-icon>
</div>
<div>
<ng-template #label><ng-content></ng-content></ng-template>
<ng-container *ngIf="value || value === 0 || value === null">
<div
class="d-flex"
[ngClass]="position === 'end' ? 'justify-content-end' : ''"
>
<ng-container *ngIf="isNumber || value === null">
<ng-container *ngIf="colorizeSign && !useAbsoluteValue">
<div *ngIf="value > 0" class="mr-1 text-success">+</div>
<div *ngIf="value < 0" class="mr-1 text-danger">-</div>
</ng-container>
<div
*ngIf="isPercent"
class="mb-0 value"
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
>
{{ formattedValue }}%
</div>
<div
*ngIf="!isPercent"
class="mb-0 value"
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
>
<ng-container *ngIf="value === null">
<span class="text-monospace text-muted">***</span>
</ng-container>
<ng-container *ngIf="value !== null">
{{ formattedValue }}
</ng-container>
</div>
<small *ngIf="currency && size === 'medium'" class="ml-1">
{{ currency }}
</small>
<div *ngIf="currency && size !== 'medium'" class="ml-1">
{{ currency }}
</div>
</ng-container>
<ng-container *ngIf="isString">
<div
class="mb-0 text-truncate value"
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
>
{{ formattedValue | titlecase }}
</div>
</ng-container>
</div>
<ng-container>
<div *ngIf="size === 'large'">
<span class="h6"
><ng-container *ngTemplateOutlet="label"></ng-container
></span>
<span *ngIf="subLabel" class="text-muted"> {{ subLabel }}</span>
</div>
<small *ngIf="currency && size === 'medium'" class="ml-1">
{{ currency }}
<small *ngIf="size !== 'large'">
<ng-container *ngTemplateOutlet="label"></ng-container>
</small>
<div *ngIf="currency && size !== 'medium'" class="ml-1">
{{ currency }}
</div>
</ng-container>
<ng-container *ngIf="isString">
<div
class="mb-0 text-truncate value"
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
>
{{ formattedValue | titlecase }}
</div>
</ng-container>
</div>
<ng-container>
<div *ngIf="size === 'large'">
<span class="h6"
><ng-container *ngTemplateOutlet="label"></ng-container
></span>
<span *ngIf="subLabel" class="text-muted"> {{ subLabel }}</span>
</div>
<small *ngIf="size !== 'large'">
<ng-container *ngTemplateOutlet="label"></ng-container>
</small>
</ng-container>
</ng-container>
<ngx-skeleton-loader
*ngIf="value === undefined"
animation="pulse"
[theme]="{
height: size === 'large' ? '2.5rem' : size === 'medium' ? '2rem' : '1.5rem',
width: '5rem'
}"
></ngx-skeleton-loader>
<ngx-skeleton-loader
*ngIf="value === undefined"
animation="pulse"
[theme]="{
height:
size === 'large' ? '2.5rem' : size === 'medium' ? '2rem' : '1.5rem',
width: '5rem'
}"
></ngx-skeleton-loader>
</div>

@ -1,6 +1,6 @@
:host {
display: flex;
flex-direction: column;
flex-direction: row;
font-variant-numeric: tabular-nums;
.h2 {

@ -16,6 +16,7 @@ import { isNumber } from 'lodash';
export class ValueComponent implements OnChanges {
@Input() colorizeSign = false;
@Input() currency = '';
@Input() icon = '';
@Input() isAbsolute = false;
@Input() isCurrency = false;
@Input() isDate = false;

@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { ValueComponent } from './value.component';
@ -8,6 +8,6 @@ import { ValueComponent } from './value.component';
declarations: [ValueComponent],
exports: [ValueComponent],
imports: [CommonModule, NgxSkeletonLoaderModule],
providers: []
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfValueModule {}

Loading…
Cancel
Save