Browse Source

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
commit
8d3954304e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      CHANGELOG.md
  2. 24
      apps/api/src/app/info/info.service.ts
  3. 16
      apps/client/src/app/pages/landing/landing-page.component.ts
  4. 53
      apps/client/src/app/pages/landing/landing-page.html
  5. 2
      apps/client/src/app/pages/landing/landing-page.module.ts
  6. 5
      apps/client/src/styles.scss
  7. 1
      libs/common/src/lib/interfaces/statistics.interface.ts
  8. 8
      libs/ui/src/lib/value/value.component.html
  9. 2
      libs/ui/src/lib/value/value.component.scss
  10. 1
      libs/ui/src/lib/value/value.component.ts
  11. 4
      libs/ui/src/lib/value/value.module.ts

2
CHANGELOG.md

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

24
apps/api/src/app/info/info.service.ts

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

16
apps/client/src/app/pages/landing/landing-page.component.ts

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

53
apps/client/src/app/pages/landing/landing-page.html

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

2
apps/client/src/app/pages/landing/landing-page.module.ts

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

5
apps/client/src/styles.scss

@ -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
libs/common/src/lib/interfaces/statistics.interface.ts

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

8
libs/ui/src/lib/value/value.component.html

@ -1,3 +1,7 @@
<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-template #label><ng-content></ng-content></ng-template>
<ng-container *ngIf="value || value === 0 || value === null"> <ng-container *ngIf="value || value === 0 || value === null">
<div <div
@ -61,7 +65,9 @@
*ngIf="value === undefined" *ngIf="value === undefined"
animation="pulse" animation="pulse"
[theme]="{ [theme]="{
height: size === 'large' ? '2.5rem' : size === 'medium' ? '2rem' : '1.5rem', height:
size === 'large' ? '2.5rem' : size === 'medium' ? '2rem' : '1.5rem',
width: '5rem' width: '5rem'
}" }"
></ngx-skeleton-loader> ></ngx-skeleton-loader>
</div>

2
libs/ui/src/lib/value/value.component.scss

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

1
libs/ui/src/lib/value/value.component.ts

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

4
libs/ui/src/lib/value/value.module.ts

@ -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…
Cancel
Save