Feature/add statistics section to landing page (#1306)
* Add pulls on Docker Hub to statistics * Add statistics to landing page * Update changelog
This commit is contained in:
parent
9562139fa6
commit
8d3954304e
@ -9,7 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### 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 an _As seen in_ section to the landing page
|
||||||
|
- Added support for an icon in the value component
|
||||||
|
|
||||||
### Changed
|
### 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> {
|
private async countGitHubContributors(): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const get = bent(
|
const get = bent(
|
||||||
@ -245,6 +266,8 @@ export class InfoService {
|
|||||||
const activeUsers1d = await this.countActiveUsers(1);
|
const activeUsers1d = await this.countActiveUsers(1);
|
||||||
const activeUsers30d = await this.countActiveUsers(30);
|
const activeUsers30d = await this.countActiveUsers(30);
|
||||||
const newUsers30d = await this.countNewUsers(30);
|
const newUsers30d = await this.countNewUsers(30);
|
||||||
|
|
||||||
|
const dockerHubPulls = await this.countDockerHubPulls();
|
||||||
const gitHubContributors = await this.countGitHubContributors();
|
const gitHubContributors = await this.countGitHubContributors();
|
||||||
const gitHubStargazers = await this.countGitHubStargazers();
|
const gitHubStargazers = await this.countGitHubStargazers();
|
||||||
const slackCommunityUsers = await this.countSlackCommunityUsers();
|
const slackCommunityUsers = await this.countSlackCommunityUsers();
|
||||||
@ -252,6 +275,7 @@ export class InfoService {
|
|||||||
statistics = {
|
statistics = {
|
||||||
activeUsers1d,
|
activeUsers1d,
|
||||||
activeUsers30d,
|
activeUsers30d,
|
||||||
|
dockerHubPulls,
|
||||||
gitHubContributors,
|
gitHubContributors,
|
||||||
gitHubStargazers,
|
gitHubStargazers,
|
||||||
newUsers30d,
|
newUsers30d,
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
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 { format } from 'date-fns';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
@ -11,6 +14,8 @@ import { Subject } from 'rxjs';
|
|||||||
export class LandingPageComponent implements OnDestroy, OnInit {
|
export class LandingPageComponent implements OnDestroy, OnInit {
|
||||||
public currentYear = format(new Date(), 'yyyy');
|
public currentYear = format(new Date(), 'yyyy');
|
||||||
public demoAuthToken: string;
|
public demoAuthToken: string;
|
||||||
|
public hasPermissionForStatistics: boolean;
|
||||||
|
public statistics: Statistics;
|
||||||
public testimonials = [
|
public testimonials = [
|
||||||
{
|
{
|
||||||
author: 'Philipp',
|
author: 'Philipp',
|
||||||
@ -36,7 +41,16 @@ export class LandingPageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
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() {}
|
public ngOnInit() {}
|
||||||
|
|
||||||
|
@ -42,7 +42,52 @@
|
|||||||
</div>
|
</div>
|
||||||
</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-12 text-center text-muted"><small>As seen in</small></div>
|
||||||
<div class="col-md-2 d-flex justify-content-center my-1">
|
<div class="col-md-2 d-flex justify-content-center my-1">
|
||||||
<a
|
<a
|
||||||
@ -110,20 +155,20 @@
|
|||||||
<div class="row my-3">
|
<div class="row my-3">
|
||||||
<div class="col-md-4 my-2">
|
<div class="col-md-4 my-2">
|
||||||
<mat-card>
|
<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
|
Get the full picture of your personal finances across multiple
|
||||||
platforms.
|
platforms.
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 my-2">
|
<div class="col-md-4 my-2">
|
||||||
<mat-card>
|
<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.
|
Use Ghostfolio anonymously and own your financial data.
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 my-2">
|
<div class="col-md-4 my-2">
|
||||||
<mat-card>
|
<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.
|
Benefit from continuous improvements through a strong community.
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
|
|||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfLogoModule } from '@ghostfolio/ui/logo';
|
import { GfLogoModule } from '@ghostfolio/ui/logo';
|
||||||
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { LandingPageRoutingModule } from './landing-page-routing.module';
|
import { LandingPageRoutingModule } from './landing-page-routing.module';
|
||||||
import { LandingPageComponent } from './landing-page.component';
|
import { LandingPageComponent } from './landing-page.component';
|
||||||
@ -13,6 +14,7 @@ import { LandingPageComponent } from './landing-page.component';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfLogoModule,
|
GfLogoModule,
|
||||||
|
GfValueModule,
|
||||||
LandingPageRoutingModule,
|
LandingPageRoutingModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
|
@ -18,6 +18,7 @@ $mat-css-light-theme-selector: '.is-light-theme';
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
--dark-background: rgb(39, 39, 39);
|
--dark-background: rgb(39, 39, 39);
|
||||||
|
--font-family-sans-serif: Roboto, 'Helvetica Neue', sans-serif;
|
||||||
--light-background: rgb(255, 255, 255);
|
--light-background: rgb(255, 255, 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,6 +147,10 @@ ngx-skeleton-loader {
|
|||||||
@include gf-table;
|
@include gf-table;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lead {
|
||||||
|
font-weight: unset;
|
||||||
|
}
|
||||||
|
|
||||||
.mat-card {
|
.mat-card {
|
||||||
&:not([class*='mat-elevation-z']) {
|
&:not([class*='mat-elevation-z']) {
|
||||||
border: 1px solid rgba(var(--dark-dividers));
|
border: 1px solid rgba(var(--dark-dividers));
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
export interface Statistics {
|
export interface Statistics {
|
||||||
activeUsers1d: number;
|
activeUsers1d: number;
|
||||||
activeUsers30d: number;
|
activeUsers30d: number;
|
||||||
|
dockerHubPulls: number;
|
||||||
gitHubContributors: number;
|
gitHubContributors: number;
|
||||||
gitHubStargazers: number;
|
gitHubStargazers: number;
|
||||||
newUsers30d: number;
|
newUsers30d: number;
|
||||||
|
@ -1,67 +1,73 @@
|
|||||||
<ng-template #label><ng-content></ng-content></ng-template>
|
<div *ngIf="icon" class="align-self-center mr-3">
|
||||||
<ng-container *ngIf="value || value === 0 || value === null">
|
<ion-icon class="h3 m-0" [name]="icon"></ion-icon>
|
||||||
<div
|
</div>
|
||||||
class="d-flex"
|
<div>
|
||||||
[ngClass]="position === 'end' ? 'justify-content-end' : ''"
|
<ng-template #label><ng-content></ng-content></ng-template>
|
||||||
>
|
<ng-container *ngIf="value || value === 0 || value === null">
|
||||||
<ng-container *ngIf="isNumber || value === null">
|
<div
|
||||||
<ng-container *ngIf="colorizeSign && !useAbsoluteValue">
|
class="d-flex"
|
||||||
<div *ngIf="value > 0" class="mr-1 text-success">+</div>
|
[ngClass]="position === 'end' ? 'justify-content-end' : ''"
|
||||||
<div *ngIf="value < 0" class="mr-1 text-danger">-</div>
|
>
|
||||||
|
<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>
|
</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>
|
</div>
|
||||||
<small *ngIf="size !== 'large'">
|
<ng-container>
|
||||||
<ng-container *ngTemplateOutlet="label"></ng-container>
|
<div *ngIf="size === 'large'">
|
||||||
</small>
|
<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>
|
</ng-container>
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ngx-skeleton-loader
|
<ngx-skeleton-loader
|
||||||
*ngIf="value === undefined"
|
*ngIf="value === undefined"
|
||||||
animation="pulse"
|
animation="pulse"
|
||||||
[theme]="{
|
[theme]="{
|
||||||
height: size === 'large' ? '2.5rem' : size === 'medium' ? '2rem' : '1.5rem',
|
height:
|
||||||
width: '5rem'
|
size === 'large' ? '2.5rem' : size === 'medium' ? '2rem' : '1.5rem',
|
||||||
}"
|
width: '5rem'
|
||||||
></ngx-skeleton-loader>
|
}"
|
||||||
|
></ngx-skeleton-loader>
|
||||||
|
</div>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
|
|
||||||
.h2 {
|
.h2 {
|
||||||
|
@ -16,6 +16,7 @@ import { isNumber } from 'lodash';
|
|||||||
export class ValueComponent implements OnChanges {
|
export class ValueComponent implements OnChanges {
|
||||||
@Input() colorizeSign = false;
|
@Input() colorizeSign = false;
|
||||||
@Input() currency = '';
|
@Input() currency = '';
|
||||||
|
@Input() icon = '';
|
||||||
@Input() isAbsolute = false;
|
@Input() isAbsolute = false;
|
||||||
@Input() isCurrency = false;
|
@Input() isCurrency = false;
|
||||||
@Input() isDate = false;
|
@Input() isDate = false;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
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 { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
import { ValueComponent } from './value.component';
|
import { ValueComponent } from './value.component';
|
||||||
@ -8,6 +8,6 @@ import { ValueComponent } from './value.component';
|
|||||||
declarations: [ValueComponent],
|
declarations: [ValueComponent],
|
||||||
exports: [ValueComponent],
|
exports: [ValueComponent],
|
||||||
imports: [CommonModule, NgxSkeletonLoaderModule],
|
imports: [CommonModule, NgxSkeletonLoaderModule],
|
||||||
providers: []
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class GfValueModule {}
|
export class GfValueModule {}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user