diff --git a/frontend/package.json b/frontend/package.json index 2573d7f58a..dc29760b07 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "1.0.129", + "version": "1.0.130", "scripts": { "install:deps": "npm install", "start": "npm install && ng serve --configuration local --open", diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index d292add176..6cbe7f5358 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -1,7 +1,11 @@ @if (showLandingPage$ | async) { - - - + @if (showLeaderboard) { + + + } @else { + + } } @else if (showLoadingWebNodePage$ | async) { } @else if (showLeaderboardPage$ | async) { @@ -29,7 +33,7 @@ [class.no-toolbar]="hideToolbar" [class.no-submenus]="subMenusLength < 2" [class.mobile]="menu.isMobile" - [class.uptime]="showUptime"> + [class.uptime]="showLeaderboard"> @if (!isDesktop) { diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index daa6282a71..8377201887 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -28,7 +28,7 @@ export class AppComponent extends StoreDispatcher implements OnInit { readonly showLeaderboardPage$: Observable = this.select$(getMergedRoute).pipe(filter(Boolean), map((route: MergedRoute) => route.url.startsWith(`/${Routes.LEADERBOARD}`))); subMenusLength: number = 0; hideToolbar: boolean = CONFIG.hideToolbar; - showUptime: boolean = CONFIG.showLeaderboard; + showLeaderboard: boolean = CONFIG.showLeaderboard; loaded: boolean; isDesktop: boolean = isDesktop(); diff --git a/frontend/src/app/app.routing.ts b/frontend/src/app/app.routing.ts index f057279939..0bd402243e 100644 --- a/frontend/src/app/app.routing.ts +++ b/frontend/src/app/app.routing.ts @@ -85,18 +85,13 @@ function generateRoutes(): Routes { title: WEBNODE_TITLE, canActivate: [landingPageGuard], }, - // { - // path: '', - // loadChildren: () => import('@leaderboard/leaderboard.module').then(m => m.LeaderboardModule), - // }, ]; if (CONFIG.showLeaderboard) { routes.push({ path: '', loadChildren: () => import('@leaderboard/leaderboard.module').then(m => m.LeaderboardModule), }); - } - if (CONFIG.showWebNodeLandingPage) { + } else if (CONFIG.showWebNodeLandingPage) { routes.push({ path: '', component: WebNodeLandingPageComponent, diff --git a/frontend/src/app/core/services/web-node.service.ts b/frontend/src/app/core/services/web-node.service.ts index a2a7f49b32..e63fe1daa5 100644 --- a/frontend/src/app/core/services/web-node.service.ts +++ b/frontend/src/app/core/services/web-node.service.ts @@ -142,7 +142,7 @@ export class WebNodeService { return throwError(() => new Error(error.message)); }), switchMap(() => this.webnode$.asObservable()), - // filter(() => CONFIG.globalConfig.heartbeats), + filter(() => CONFIG.globalConfig.heartbeats), switchMap(() => timer(0, 60000)), switchMap(() => this.heartBeat$), switchMap(heartBeat => this.firestore.addHeartbeat(heartBeat)), diff --git a/frontend/src/app/features/leaderboard/leaderboard-header/leaderboard-header.component.html b/frontend/src/app/features/leaderboard/leaderboard-header/leaderboard-header.component.html index 0f448f9ca0..5f204ed778 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-header/leaderboard-header.component.html +++ b/frontend/src/app/features/leaderboard/leaderboard-header/leaderboard-header.component.html @@ -6,8 +6,8 @@ [@dropdownAnimation]="isMenuOpen ? 'open' : 'closed'" [class.open]="isMenuOpen" (clickOutside)="closeMenu()"> - Mina Web Node Leaderboard - Round 1 Details + Program Details + Prize Draw & Tie-Break Process diff --git a/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.html b/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.html index 81b7e477a0..85befa2146 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.html +++ b/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.html @@ -8,7 +8,10 @@
- + + @if (canDownloadCSV) { + + }
@@ -36,6 +39,17 @@ + +
+
+ +
+
+
+

Download Public Keys that qualify for the following Prizes:

diff --git a/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.ts b/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.ts index f865a4d74e..550c29022f 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.ts +++ b/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.ts @@ -41,6 +41,7 @@ import { LeaderboardService } from '@leaderboard/leaderboard.service'; export class LeaderboardPageComponent extends StoreDispatcher implements OnInit, AfterViewInit { isExpanded = false; showBanner: boolean = false; + canDownloadCSV = localStorage.getItem('download_leaderboard') === 'true'; private readonly SCROLL_THRESHOLD = 100; @ViewChild('scrollContainer') private scrollContainer!: ElementRef; @@ -90,4 +91,8 @@ export class LeaderboardPageComponent extends StoreDispatcher implements OnInit, downloadMostProducedBlocks(): void { this.leaderboardService.downloadMostProducedBlocks(); } + + downloadAll(): void { + this.leaderboardService.downloadAll(); + } } diff --git a/frontend/src/app/features/leaderboard/leaderboard.routing.ts b/frontend/src/app/features/leaderboard/leaderboard.routing.ts index 69b819e42c..1a72308dba 100644 --- a/frontend/src/app/features/leaderboard/leaderboard.routing.ts +++ b/frontend/src/app/features/leaderboard/leaderboard.routing.ts @@ -29,7 +29,7 @@ const routes: Routes = [ }, { path: '**', - redirectTo: '', + redirectTo: 'leaderboard', }, ]; diff --git a/frontend/src/app/features/leaderboard/leaderboard.service.ts b/frontend/src/app/features/leaderboard/leaderboard.service.ts index 793d6732d1..15ec034013 100644 --- a/frontend/src/app/features/leaderboard/leaderboard.service.ts +++ b/frontend/src/app/features/leaderboard/leaderboard.service.ts @@ -4,6 +4,7 @@ import { HeartbeatSummary } from '@shared/types/leaderboard/heartbeat-summary.ty import { collection, collectionData, CollectionReference, Firestore, getDocs } from '@angular/fire/firestore'; import { WebNodeService } from '@core/services/web-node.service'; import { getElapsedTimeInMinsAndHours } from '@shared/helpers/date.helper'; +import { ONE_THOUSAND, toReadableDate } from '@openmina/shared'; @Injectable({ providedIn: 'root', @@ -35,7 +36,7 @@ export class LeaderboardService { return ({ publicKey: score['publicKey'], blocksProduced: score['blocksProduced'], - isActive: score['lastUpdated'] > Date.now() - 120000, + isActive: score['lastHeartbeat'] * ONE_THOUSAND > (Date.now() - 12000), uptimePercentage: this.getUptimePercentage(score['score'], this.maxScoreRightNow), uptimePrize: false, blocksPrize: false, @@ -232,4 +233,50 @@ export class LeaderboardService { URL.revokeObjectURL(url); } + + async downloadAll(): Promise { + const querySnapshot = await getDocs(this.scoresCollection); + const scoresData: any[] = []; + + querySnapshot.forEach((doc) => { + scoresData.push({ id: doc.id, ...doc.data() }); + }); + + const csvRows = []; + + let filteredData = scoresData + .map(row => ({ + publicKey: row.publicKey, + score: row.score + ' / ' + this.maxScoreRightNow, + uptime: this.getUptimePercentage(row.score, this.maxScoreRightNow) + '%', + uptimeTime: row.score, + producedBlocks: row.blocksProduced, + lastUpdated: toReadableDate(row.lastUpdated * ONE_THOUSAND), + })); + filteredData = [...filteredData].sort((a, b) => b.uptimeTime - a.uptimeTime); + + const headers = ['publicKey', 'score', 'uptime', /*'lastUpdated',*/ 'producedBlocks'].map(header => this.camelCaseToTitle(header)); + csvRows.push(headers.join(',')); + + // Map rows + filteredData.forEach((row: any) => { + const values = headers.map(header => { + const key = header.charAt(0).toLowerCase() + header.slice(1); // Convert to corresponding key + const escape = ('' + row[key.replace(' ', '')]).replace(/"/g, '\\"'); + return `"${escape}"`; + }); + csvRows.push(values.join(',')); + }); + + const csvString = csvRows.join('\n'); + const blob = new Blob([csvString], { type: 'text/csv' }); + const url = URL.createObjectURL(blob); + + const link = document.createElement('a'); + link.href = url; + link.download = `export_${new Date().toISOString().replace(/:/g, '-')}.csv`; + link.click(); + + URL.revokeObjectURL(url); + } } diff --git a/frontend/src/index.html b/frontend/src/index.html index 94afde2475..2dcc99efab 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -49,11 +49,11 @@