diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 6f435c80a..76da3254d 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -3,30 +3,42 @@ import { Routes, RouterModule } from '@angular/router'; import { BlockchainComponent } from './blockchain/blockchain.component'; import { AboutComponent } from './about/about.component'; import { StatisticsComponent } from './statistics/statistics.component'; +import { TelevisionComponent } from './television/television.component'; +import { MasterPageComponent } from './master-page/master-page.component'; const routes: Routes = [ { path: '', - children: [], - component: BlockchainComponent + component: MasterPageComponent, + children: [ + { + path: '', + children: [], + component: BlockchainComponent + }, + { + path: 'tx/:id', + children: [], + component: BlockchainComponent + }, + { + path: 'about', + children: [], + component: AboutComponent + }, + { + path: 'statistics', + component: StatisticsComponent, + }, + { + path: 'graphs', + component: StatisticsComponent, + }, + ], }, { - path: 'tx/:id', - children: [], - component: BlockchainComponent - }, - { - path: 'about', - children: [], - component: AboutComponent - }, - { - path: 'statistics', - component: StatisticsComponent, - }, - { - path: 'graphs', - component: StatisticsComponent, + path: 'tv', + component: TelevisionComponent, }, { path: '**', diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index aca28bbea..90c6b6463 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -1,32 +1 @@ -
- -
- -
- \ No newline at end of file diff --git a/frontend/src/app/app.component.scss b/frontend/src/app/app.component.scss index bdaf25149..e69de29bb 100644 --- a/frontend/src/app/app.component.scss +++ b/frontend/src/app/app.component.scss @@ -1,28 +0,0 @@ -li.nav-item.active { - background-color: #653b9c; -} - -li.nav-item { - padding: 10px; -} - -.navbar { - z-index: 100; -} - -@media (min-width: 768px) { - .navbar { - padding: 0rem 1rem; - } - li.nav-item { - padding: 20px; - } -} - -.logo { - margin-left: 40px; -} - -li.nav-item a { - color: #ffffff; -} diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 98ad9de95..d198fcdfa 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,52 +1,10 @@ -import { Component, OnInit } from '@angular/core'; -import { MemPoolService } from './services/mem-pool.service'; -import { Router } from '@angular/router'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) -export class AppComponent implements OnInit { - navCollapsed = false; - isOffline = false; - searchForm: FormGroup; - - constructor( - private memPoolService: MemPoolService, - private router: Router, - private formBuilder: FormBuilder, - ) { } - - ngOnInit() { - this.searchForm = this.formBuilder.group({ - txId: ['', Validators.pattern('^[a-fA-F0-9]{64}$')], - }); - - this.memPoolService.isOffline$ - .subscribe((state) => { - this.isOffline = state; - }); - } - - collapse(): void { - this.navCollapsed = !this.navCollapsed; - } - - search() { - const txId = this.searchForm.value.txId; - if (txId) { - if (window.location.pathname === '/' || window.location.pathname.substr(0, 4) === '/tx/') { - window.history.pushState({}, '', `/tx/${txId}`); - } else { - this.router.navigate(['/tx/', txId]); - } - this.memPoolService.txIdSearch$.next(txId); - this.searchForm.setValue({ - txId: '', - }); - this.collapse(); - } - } +export class AppComponent { + constructor() { } } diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 70d488ef2..8431e5ef6 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -14,8 +14,11 @@ import { ReactiveFormsModule } from '@angular/forms'; import { BlockModalComponent } from './blockchain-blocks/block-modal/block-modal.component'; import { StatisticsComponent } from './statistics/statistics.component'; import { ProjectedBlockModalComponent } from './blockchain-projected-blocks/projected-block-modal/projected-block-modal.component'; +import { TelevisionComponent } from './television/television.component'; import { BlockchainBlocksComponent } from './blockchain-blocks/blockchain-blocks.component'; import { BlockchainProjectedBlocksComponent } from './blockchain-projected-blocks/blockchain-projected-blocks.component'; +import { ApiService } from './services/api.service'; +import { MasterPageComponent } from './master-page/master-page.component'; @NgModule({ declarations: [ @@ -27,8 +30,10 @@ import { BlockchainProjectedBlocksComponent } from './blockchain-projected-block TxBubbleComponent, BlockModalComponent, ProjectedBlockModalComponent, + TelevisionComponent, BlockchainBlocksComponent, BlockchainProjectedBlocksComponent, + MasterPageComponent, ], imports: [ ReactiveFormsModule, @@ -38,6 +43,7 @@ import { BlockchainProjectedBlocksComponent } from './blockchain-projected-block SharedModule, ], providers: [ + ApiService, MemPoolService, ], entryComponents: [ diff --git a/frontend/src/app/blockchain-blocks/blockchain-blocks.component.ts b/frontend/src/app/blockchain-blocks/blockchain-blocks.component.ts index aef5b4c7e..3dc52ed66 100644 --- a/frontend/src/app/blockchain-blocks/blockchain-blocks.component.ts +++ b/frontend/src/app/blockchain-blocks/blockchain-blocks.component.ts @@ -21,7 +21,10 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { ngOnInit() { this.blocksSubscription = this.memPoolService.blocks$ - .subscribe((block) => this.blocks.unshift(block)); + .subscribe((block) => { + this.blocks.unshift(block); + this.blocks = this.blocks.slice(0, 8); + }); } ngOnDestroy() { diff --git a/frontend/src/app/master-page/master-page.component.html b/frontend/src/app/master-page/master-page.component.html new file mode 100644 index 000000000..aeba9e2bc --- /dev/null +++ b/frontend/src/app/master-page/master-page.component.html @@ -0,0 +1,32 @@ +
+ +
+ +
+ + \ No newline at end of file diff --git a/frontend/src/app/master-page/master-page.component.scss b/frontend/src/app/master-page/master-page.component.scss new file mode 100644 index 000000000..bdaf25149 --- /dev/null +++ b/frontend/src/app/master-page/master-page.component.scss @@ -0,0 +1,28 @@ +li.nav-item.active { + background-color: #653b9c; +} + +li.nav-item { + padding: 10px; +} + +.navbar { + z-index: 100; +} + +@media (min-width: 768px) { + .navbar { + padding: 0rem 1rem; + } + li.nav-item { + padding: 20px; + } +} + +.logo { + margin-left: 40px; +} + +li.nav-item a { + color: #ffffff; +} diff --git a/frontend/src/app/master-page/master-page.component.ts b/frontend/src/app/master-page/master-page.component.ts new file mode 100644 index 000000000..ac9368ac6 --- /dev/null +++ b/frontend/src/app/master-page/master-page.component.ts @@ -0,0 +1,54 @@ +import { Component, OnInit } from '@angular/core'; +import { MemPoolService } from '../services/mem-pool.service'; +import { Router } from '@angular/router'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +@Component({ + selector: 'app-master-page', + templateUrl: './master-page.component.html', + styleUrls: ['./master-page.component.scss'] +}) +export class MasterPageComponent implements OnInit { + + navCollapsed = false; + isOffline = false; + searchForm: FormGroup; + + constructor( + private memPoolService: MemPoolService, + private router: Router, + private formBuilder: FormBuilder, + ) { } + + ngOnInit() { + this.searchForm = this.formBuilder.group({ + txId: ['', Validators.pattern('^[a-fA-F0-9]{64}$')], + }); + + this.memPoolService.isOffline$ + .subscribe((state) => { + this.isOffline = state; + }); + } + + collapse(): void { + this.navCollapsed = !this.navCollapsed; + } + + search() { + const txId = this.searchForm.value.txId; + if (txId) { + if (window.location.pathname === '/' || window.location.pathname.substr(0, 4) === '/tx/') { + window.history.pushState({}, '', `/tx/${txId}`); + } else { + this.router.navigate(['/tx/', txId]); + } + this.memPoolService.txIdSearch$.next(txId); + this.searchForm.setValue({ + txId: '', + }); + this.collapse(); + } + } + +} diff --git a/frontend/src/app/statistics/statistics.component.ts b/frontend/src/app/statistics/statistics.component.ts index 1f0aca0da..de1d953d3 100644 --- a/frontend/src/app/statistics/statistics.component.ts +++ b/frontend/src/app/statistics/statistics.component.ts @@ -215,13 +215,6 @@ export class StatisticsComponent implements OnInit { }; } - getTimeToNextTenMinutes(): number { - const now = new Date(); - const nextInterval = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), - Math.floor(now.getMinutes() / 10) * 10 + 10, 0, 0); - return nextInterval.getTime() - now.getTime(); - } - generateArray(mempoolStats: IMempoolStats[]) { const logFees = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200, 250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000]; diff --git a/frontend/src/app/television/television.component.html b/frontend/src/app/television/television.component.html new file mode 100644 index 000000000..2b1db4c31 --- /dev/null +++ b/frontend/src/app/television/television.component.html @@ -0,0 +1,25 @@ +
+ +
+
+
+ +
+ + +
+ +
+
+ + + +
+
+ +
+ +
\ No newline at end of file diff --git a/frontend/src/app/television/television.component.scss b/frontend/src/app/television/television.component.scss new file mode 100644 index 000000000..b510145c3 --- /dev/null +++ b/frontend/src/app/television/television.component.scss @@ -0,0 +1,35 @@ +#tv-wrapper { + height: 100%; + margin: 10px; + margin-top: 20px; +} + +.blockchain-wrapper { + overflow: hidden; +} + +.position-container { + position: absolute; + left: 50%; + bottom: 150px; +} + +#divider { + width: 3px; + height: 175px; + left: 0; + top: -40px; + background-image: url('/assets/divider-new.png'); + background-repeat: repeat-y; + position: absolute; +} + +#divider > img { + position: absolute; + left: -100px; + top: -28px; +} + +.chart-holder { + height: calc(100% - 220px); +} \ No newline at end of file diff --git a/frontend/src/app/television/television.component.ts b/frontend/src/app/television/television.component.ts new file mode 100644 index 000000000..4c3ee43ff --- /dev/null +++ b/frontend/src/app/television/television.component.ts @@ -0,0 +1,118 @@ +import { Component, OnInit, LOCALE_ID, Inject } from '@angular/core'; +import { ApiService } from '../services/api.service'; +import { formatDate } from '@angular/common'; +import { BytesPipe } from '../shared/pipes/bytes-pipe/bytes.pipe'; + +import * as Chartist from 'chartist'; +import { IMempoolStats } from '../blockchain/interfaces'; +import { MemPoolService } from '../services/mem-pool.service'; + +@Component({ + selector: 'app-television', + templateUrl: './television.component.html', + styleUrls: ['./television.component.scss'] +}) +export class TelevisionComponent implements OnInit { + loading = true; + + mempoolStats: IMempoolStats[] = []; + mempoolVsizeFeesData: any; + mempoolVsizeFeesOptions: any; + + constructor( + private apiService: ApiService, + @Inject(LOCALE_ID) private locale: string, + private bytesPipe: BytesPipe, + private memPoolService: MemPoolService, + ) { } + + ngOnInit() { + this.apiService.sendWebSocket({'action': 'want', data: ['projected-blocks', 'live-2h-chart']}); + + const labelInterpolationFnc = (value: any, index: any) => { + return index % 6 === 0 ? formatDate(value, 'HH:mm', this.locale) : null; + }; + + this.mempoolVsizeFeesOptions = { + showArea: true, + showLine: false, + fullWidth: true, + showPoint: false, + low: 0, + axisX: { + labelInterpolationFnc: labelInterpolationFnc, + offset: 40 + }, + axisY: { + labelInterpolationFnc: (value: number): any => { + return this.bytesPipe.transform(value); + }, + offset: 50 + }, + plugins: [ + Chartist.plugins.ctTargetLine({ + value: 1000000 + }), + ] + }; + + this.apiService.list2HStatistics$() + .subscribe((mempoolStats) => { + this.mempoolStats = mempoolStats; + this.handleNewMempoolData(this.mempoolStats.concat([])); + this.loading = false; + }); + + this.memPoolService.live2Chart$ + .subscribe((mempoolStats) => { + this.mempoolStats.unshift(mempoolStats); + this.mempoolStats = this.mempoolStats.slice(0, this.mempoolStats.length - 1); + this.handleNewMempoolData(this.mempoolStats.concat([])); + }); + } + + handleNewMempoolData(mempoolStats: IMempoolStats[]) { + mempoolStats.reverse(); + const labels = mempoolStats.map(stats => stats.added); + + const finalArrayVbyte = this.generateArray(mempoolStats); + + // Remove the 0-1 fee vbyte since it's practially empty + finalArrayVbyte.shift(); + + this.mempoolVsizeFeesData = { + labels: labels, + series: finalArrayVbyte + }; + } + + generateArray(mempoolStats: IMempoolStats[]) { + const logFees = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200, + 250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000]; + + logFees.reverse(); + + const finalArray: number[][] = []; + let feesArray: number[] = []; + + logFees.forEach((fee) => { + feesArray = []; + mempoolStats.forEach((stats) => { + // @ts-ignore + const theFee = stats['vsize_' + fee]; + if (theFee) { + feesArray.push(parseInt(theFee, 10)); + } else { + feesArray.push(0); + } + }); + if (finalArray.length) { + feesArray = feesArray.map((value, i) => value + finalArray[finalArray.length - 1][i]); + } + finalArray.push(feesArray); + }); + finalArray.reverse(); + return finalArray; + } + +} diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 129ca1c2e..deec4d859 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -66,6 +66,10 @@ body { margin-bottom: 60px; } +html, body { + height: 100%; +} + @media (min-width: 768px) { body.disable-scroll { overflow: hidden;