diff --git a/frontend/src/app/components/pool/pool-preview.component.html b/frontend/src/app/components/pool/pool-preview.component.html new file mode 100644 index 000000000..93e65aeae --- /dev/null +++ b/frontend/src/app/components/pool/pool-preview.component.html @@ -0,0 +1,34 @@ +
+ + mining pool + +
+
+

{{ poolStats.pool.name }}

+
+
+ + +
+
+
+
+
+
Tags
+
{{ poolStats.pool.regexes }}
+
+
+
Hashrate
+
{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}
+
+
+
+
+
+
+
+ + +
~
+
\ No newline at end of file diff --git a/frontend/src/app/components/pool/pool-preview.component.scss b/frontend/src/app/components/pool/pool-preview.component.scss new file mode 100644 index 000000000..533bac4af --- /dev/null +++ b/frontend/src/app/components/pool/pool-preview.component.scss @@ -0,0 +1,78 @@ +.stats { + display: flex; + flex-direction: row; + justify-content: center; + align-items: flex-start; + width: 100%; + max-width: 100%; + margin: 15px 0; + font-size: 32px; + overflow: hidden; + + .stat-box { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: baseline; + justify-content: space-between; + width: 100%; + margin-left: 15px; + background: #181b2d; + padding: 0.75rem; + width: 0; + flex-grow: 1; + + &:first-child { + margin-left: 0; + } + + .label { + flex-shrink: 0; + flex-grow: 0; + margin-right: 1em; + } + .data { + flex-shrink: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } +} + +.chart { + width: 100%; + height: 315px; + background: #181b2d; +} + +.row { + margin-right: 0; +} + +.full-width-row { + padding-left: 15px; + flex-wrap: nowrap; +} + +.logo-wrapper { + position: relative; + width: 62px; + height: 62px; + margin-left: 1em; + + img { + position: absolute; + right: 0; + top: 0; + background: #24273e; + + &.noimg { + opacity: 0; + } + } +} + +::ng-deep .symbol { + font-size: 24px; +} diff --git a/frontend/src/app/components/pool/pool-preview.component.ts b/frontend/src/app/components/pool/pool-preview.component.ts new file mode 100644 index 000000000..2799dc34b --- /dev/null +++ b/frontend/src/app/components/pool/pool-preview.component.ts @@ -0,0 +1,187 @@ +import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { EChartsOption, graphic } from 'echarts'; +import { Observable, of } from 'rxjs'; +import { map, switchMap, catchError } from 'rxjs/operators'; +import { PoolStat } from 'src/app/interfaces/node-api.interface'; +import { ApiService } from 'src/app/services/api.service'; +import { StateService } from 'src/app/services/state.service'; +import { formatNumber } from '@angular/common'; +import { SeoService } from 'src/app/services/seo.service'; +import { OpenGraphService } from 'src/app/services/opengraph.service'; + +@Component({ + selector: 'app-pool-preview', + templateUrl: './pool-preview.component.html', + styleUrls: ['./pool-preview.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class PoolPreviewComponent implements OnInit { + formatNumber = formatNumber; + poolStats$: Observable; + isLoading = true; + imageLoaded = false; + lastImgSrc: string = ''; + + chartOptions: EChartsOption = {}; + chartInitOptions = { + renderer: 'svg', + }; + + slug: string = undefined; + + constructor( + @Inject(LOCALE_ID) public locale: string, + private apiService: ApiService, + private route: ActivatedRoute, + public stateService: StateService, + private seoService: SeoService, + private openGraphService: OpenGraphService, + ) { + } + + ngOnInit(): void { + this.poolStats$ = this.route.params.pipe(map((params) => params.slug)) + .pipe( + switchMap((slug: any) => { + this.isLoading = true; + this.imageLoaded = false; + this.slug = slug; + this.openGraphService.waitFor('pool-hash-' + this.slug); + this.openGraphService.waitFor('pool-stats-' + this.slug); + this.openGraphService.waitFor('pool-chart-' + this.slug); + this.openGraphService.waitFor('pool-img-' + this.slug); + return this.apiService.getPoolHashrate$(this.slug) + .pipe( + switchMap((data) => { + this.isLoading = false; + this.prepareChartOptions(data.map(val => [val.timestamp * 1000, val.avgHashrate])); + this.openGraphService.waitOver('pool-hash-' + this.slug); + return [slug]; + }), + catchError(() => { + this.isLoading = false; + this.openGraphService.fail('pool-hash-' + this.slug); + return of([slug]); + }) + ); + }), + switchMap((slug) => { + return this.apiService.getPoolStats$(slug).pipe( + catchError(() => { + this.isLoading = false; + this.openGraphService.fail('pool-stats-' + this.slug); + return of(null); + }) + ); + }), + map((poolStats) => { + if (poolStats == null) { + return null; + } + + this.seoService.setTitle(poolStats.pool.name); + let regexes = '"'; + for (const regex of poolStats.pool.regexes) { + regexes += regex + '", "'; + } + poolStats.pool.regexes = regexes.slice(0, -3); + poolStats.pool.addresses = poolStats.pool.addresses; + + if (poolStats.reportedHashrate) { + poolStats.luck = poolStats.estimatedHashrate / poolStats.reportedHashrate * 100; + } + + this.openGraphService.waitOver('pool-stats-' + this.slug); + + const logoSrc = `/resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'; + if (logoSrc === this.lastImgSrc) { + this.openGraphService.waitOver('pool-img-' + this.slug); + } + this.lastImgSrc = logoSrc; + return Object.assign({ + logo: logoSrc + }, poolStats); + }), + catchError(() => { + this.isLoading = false; + this.openGraphService.fail('pool-stats-' + this.slug); + return of(null); + }) + ); + } + + prepareChartOptions(data) { + let title: object; + if (data.length === 0) { + title = { + textStyle: { + color: 'grey', + fontSize: 15 + }, + text: $localize`:@@23555386d8af1ff73f297e89dd4af3f4689fb9dd:Indexing blocks`, + left: 'center', + top: 'center' + }; + } + + this.chartOptions = { + title: title, + animation: false, + color: [ + new graphic.LinearGradient(0, 0, 0, 0.65, [ + { offset: 0, color: '#F4511E' }, + { offset: 0.25, color: '#FB8C00' }, + { offset: 0.5, color: '#FFB300' }, + { offset: 0.75, color: '#FDD835' }, + { offset: 1, color: '#7CB342' } + ]), + '#D81B60', + ], + grid: { + left: 15, + right: 15, + bottom: 15, + top: 15, + show: false, + }, + xAxis: data.length === 0 ? undefined : { + type: 'time', + show: false, + }, + yAxis: data.length === 0 ? undefined : [ + { + type: 'value', + show: false, + }, + ], + series: data.length === 0 ? undefined : [ + { + zlevel: 0, + name: 'Hashrate', + showSymbol: false, + symbol: 'none', + data: data, + type: 'line', + lineStyle: { + width: 4, + }, + }, + ], + }; + } + + onChartReady(): void { + this.openGraphService.waitOver('pool-chart-' + this.slug); + } + + onImageLoad(): void { + this.imageLoaded = true; + this.openGraphService.waitOver('pool-img-' + this.slug); + } + + onImageFail(): void { + this.imageLoaded = false; + this.openGraphService.waitOver('pool-img-' + this.slug); + } +} diff --git a/frontend/src/app/previews.module.ts b/frontend/src/app/previews.module.ts index 166670ced..2e8dbdc75 100644 --- a/frontend/src/app/previews.module.ts +++ b/frontend/src/app/previews.module.ts @@ -7,20 +7,22 @@ import { PreviewsRoutingModule } from './previews.routing.module'; import { TransactionPreviewComponent } from './components/transaction/transaction-preview.component'; import { BlockPreviewComponent } from './components/block/block-preview.component'; import { AddressPreviewComponent } from './components/address/address-preview.component'; +import { PoolPreviewComponent } from './components/pool/pool-preview.component'; import { MasterPagePreviewComponent } from './components/master-page-preview/master-page-preview.component'; @NgModule({ declarations: [ TransactionPreviewComponent, BlockPreviewComponent, AddressPreviewComponent, + PoolPreviewComponent, MasterPagePreviewComponent, ], imports: [ CommonModule, SharedModule, RouterModule, - GraphsModule, PreviewsRoutingModule, + GraphsModule, ], }) export class PreviewsModule { } diff --git a/frontend/src/app/previews.routing.module.ts b/frontend/src/app/previews.routing.module.ts index 5ac13c36d..c2ad8db5f 100644 --- a/frontend/src/app/previews.routing.module.ts +++ b/frontend/src/app/previews.routing.module.ts @@ -3,6 +3,7 @@ import { RouterModule, Routes } from '@angular/router'; import { TransactionPreviewComponent } from './components/transaction/transaction-preview.component'; import { BlockPreviewComponent } from './components/block/block-preview.component'; import { AddressPreviewComponent } from './components/address/address-preview.component'; +import { PoolPreviewComponent } from './components/pool/pool-preview.component'; import { MasterPagePreviewComponent } from './components/master-page-preview/master-page-preview.component'; const routes: Routes = [ @@ -24,6 +25,10 @@ const routes: Routes = [ children: [], component: TransactionPreviewComponent }, + { + path: 'mining/pool/:slug', + component: PoolPreviewComponent + }, { path: 'lightning', loadChildren: () => import('./lightning/lightning-previews.module').then(m => m.LightningPreviewsModule) diff --git a/unfurler/src/routes.ts b/unfurler/src/routes.ts index 4c25bf93b..3dfa66b5f 100644 --- a/unfurler/src/routes.ts +++ b/unfurler/src/routes.ts @@ -61,7 +61,16 @@ const routes = { }, mining: { title: "Mining", - fallbackImg: '/resources/previews/mining.png' + fallbackImg: '/resources/previews/mining.png', + routes: { + pool: { + render: true, + params: 1, + getTitle(path) { + return `Mining Pool: ${path[0]}`; + } + } + } } };