diff --git a/frontend/package.json b/frontend/package.json index 9003e5757a..5b184eab7b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "1.0.103", + "version": "1.0.121", "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 3b43ad7624..d292add176 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -28,7 +28,8 @@ class="overflow-hidden" [class.no-toolbar]="hideToolbar" [class.no-submenus]="subMenusLength < 2" - [class.mobile]="menu.isMobile"> + [class.mobile]="menu.isMobile" + [class.uptime]="showUptime"> @if (!isDesktop) { diff --git a/frontend/src/app/app.component.scss b/frontend/src/app/app.component.scss index c661e58c8c..2044b515e5 100644 --- a/frontend/src/app/app.component.scss +++ b/frontend/src/app/app.component.scss @@ -76,8 +76,10 @@ mat-sidenav-content { margin-bottom: 4px; border-top-right-radius: 6px; - &.no-toolbar { - height: calc(100% - #{$subMenus} - #{$tabs}); + + &.uptime { + $toolbar: 130px; + height: calc(100% - #{$toolbar} - #{$subMenus} - #{$tabs}); } &.no-submenus { @@ -86,6 +88,14 @@ mat-sidenav-content { &.no-toolbar { height: 100%; } + + &.uptime { + height: calc(100% - #{$toolbar} - #{$subMenus}); + } + } + + &.no-toolbar { + height: calc(100% - #{$subMenus} - #{$tabs}); } } } diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 0e5f1f9deb..9a3447549a 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -28,6 +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; loaded: boolean; isDesktop: boolean = isDesktop(); @@ -54,31 +55,30 @@ export class AppComponent extends StoreDispatcher implements OnInit { localStorage.setItem('webnodeArgs', args); } } + this.select(getMergedRoute, () => { + this.loaded = true; + this.detect(); + }, filter(Boolean), take(1)); - this.select( - getMergedRoute, - () => this.initAppFunctionalities(), - filter(Boolean), - take(1), - filter((route: MergedRoute) => route.url !== '/' && !route.url.startsWith('/?') && !route.url.startsWith('/leaderboard')), - ); - this.select( - getMergedRoute, - () => { - this.loaded = true; - this.detect(); - }, - filter(Boolean), - take(1), - ); + if (CONFIG.showLeaderboard && CONFIG.showWebNodeLandingPage) { + /* frontend with some landing page */ + this.select(getMergedRoute, () => { + this.initAppFunctionalities(); + }, filter((route: MergedRoute) => route?.url.startsWith('/loading-web-node')), take(1)); + + } else if (!CONFIG.showLeaderboard && !CONFIG.showWebNodeLandingPage) { + /* normal frontend (no landing pages) */ + this.initAppFunctionalities(); + } } goToWebNode(): void { - this.router.navigate([Routes.LOADING_WEB_NODE], { queryParamsHandling: 'merge' }); + // this.router.navigate([Routes.LOADING_WEB_NODE], { queryParamsHandling: 'merge' }); this.initAppFunctionalities(); } private initAppFunctionalities(): void { + console.log('initAppFunctionalities'); if (this.webNodeService.hasWebNodeConfig() && !this.webNodeService.isWebNodeLoaded()) { if (!getWindow()?.location.href.includes(`/${Routes.LOADING_WEB_NODE}`)) { this.router.navigate([Routes.LOADING_WEB_NODE], { queryParamsHandling: 'preserve' }); diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 734d520c3d..dba557a132 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -43,6 +43,7 @@ import { BlockProductionPillComponent } from '@app/layout/block-production-pill/ import { MenuTabsComponent } from '@app/layout/menu-tabs/menu-tabs.component'; import { getFirestore, provideFirestore } from '@angular/fire/firestore'; import { LeaderboardModule } from '@leaderboard/leaderboard.module'; +import { UptimePillComponent } from '@app/layout/uptime-pill/uptime-pill.component'; registerLocaleData(localeFr, 'fr'); registerLocaleData(localeEn, 'en'); @@ -166,6 +167,7 @@ export class AppGlobalErrorhandler implements ErrorHandler { BlockProductionPillComponent, MenuTabsComponent, LeaderboardModule, + UptimePillComponent, ], providers: [ THEME_PROVIDER, diff --git a/frontend/src/app/app.routing.ts b/frontend/src/app/app.routing.ts index 1a5d8eca6d..f057279939 100644 --- a/frontend/src/app/app.routing.ts +++ b/frontend/src/app/app.routing.ts @@ -2,6 +2,9 @@ import { NgModule } from '@angular/core'; import { NoPreloading, RouterModule, Routes } from '@angular/router'; import { CONFIG, getFirstFeature } from '@shared/constants/config'; import { WebNodeLandingPageComponent } from '@app/layout/web-node-landing-page/web-node-landing-page.component'; +import { getMergedRoute, MergedRoute } from '@openmina/shared'; +import { filter, take } from 'rxjs'; +import { landingPageGuard } from '@shared/guards/landing-page.guard'; const APP_TITLE: string = 'Open Mina'; @@ -24,6 +27,7 @@ function generateRoutes(): Routes { path: 'dashboard', loadChildren: () => import('@dashboard/dashboard.module').then(m => m.DashboardModule), title: DASHBOARD_TITLE, + canActivate: [landingPageGuard], }, { path: 'nodes', @@ -45,6 +49,7 @@ function generateRoutes(): Routes { path: 'state', loadChildren: () => import('@state/state.module').then(m => m.StateModule), title: STATE_TITLE, + canActivate: [landingPageGuard], }, { path: 'snarks', @@ -55,16 +60,19 @@ function generateRoutes(): Routes { path: 'block-production', loadChildren: () => import('@block-production/block-production.module').then(m => m.BlockProductionModule), title: BLOCK_PRODUCTION_TITLE, + canActivate: [landingPageGuard], }, { path: 'mempool', loadChildren: () => import('@mempool/mempool.module').then(m => m.MempoolModule), title: MEMPOOL_TITLE, + canActivate: [landingPageGuard], }, { path: 'benchmarks', loadChildren: () => import('@benchmarks/benchmarks.module').then(m => m.BenchmarksModule), title: BENCHMARKS_TITLE, + canActivate: [landingPageGuard], }, { path: 'fuzzing', @@ -75,12 +83,19 @@ function generateRoutes(): Routes { path: 'loading-web-node', loadChildren: () => import('@web-node/web-node.module').then(m => m.WebNodeModule), 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) { routes.push({ path: '', diff --git a/frontend/src/app/core/helpers/file-progress.helper.ts b/frontend/src/app/core/helpers/file-progress.helper.ts index 890310def7..fed9a8d31f 100644 --- a/frontend/src/app/core/helpers/file-progress.helper.ts +++ b/frontend/src/app/core/helpers/file-progress.helper.ts @@ -1,7 +1,7 @@ import { BehaviorSubject } from 'rxjs'; import { safelyExecuteInBrowser } from '@openmina/shared'; -const WASM_FILE_SIZE = 31705944; +const WASM_FILE_SIZE = 31556926; class AssetMonitor { readonly downloads: Map = new Map(); diff --git a/frontend/src/app/core/services/firestore.service.ts b/frontend/src/app/core/services/firestore.service.ts index 1073919e4e..3871c5a6d9 100644 --- a/frontend/src/app/core/services/firestore.service.ts +++ b/frontend/src/app/core/services/firestore.service.ts @@ -11,7 +11,7 @@ import { DocumentData, } from '@angular/fire/firestore'; import { HttpClient } from '@angular/common/http'; -import { Observable } from 'rxjs'; +import { catchError, EMPTY, Observable } from 'rxjs'; @Injectable({ providedIn: 'root', @@ -27,7 +27,12 @@ export class FirestoreService { addHeartbeat(data: any): Observable { console.log('Posting to cloud function:', data); - return this.http.post(this.cloudFunctionUrl, { data }); + return this.http.post(this.cloudFunctionUrl, { data }).pipe( + catchError(error => { + console.error('Error while posting to cloud function:', error); + return error; + }), + ); } updateHeartbeat(id: string, data: any): Promise { diff --git a/frontend/src/app/core/services/rust.service.ts b/frontend/src/app/core/services/rust.service.ts index 4a3fe69be5..0e6d26c04f 100644 --- a/frontend/src/app/core/services/rust.service.ts +++ b/frontend/src/app/core/services/rust.service.ts @@ -15,6 +15,7 @@ export class RustService { private webNodeService: WebNodeService) {} changeRustNode(node: MinaNode): void { + console.log('Changing Rust node to:', node); this.node = node; } diff --git a/frontend/src/app/core/services/web-node.service.ts b/frontend/src/app/core/services/web-node.service.ts index 549afe48cb..0a1c20e12c 100644 --- a/frontend/src/app/core/services/web-node.service.ts +++ b/frontend/src/app/core/services/web-node.service.ts @@ -60,6 +60,7 @@ export class WebNodeService { } loadWasm$(): Observable { + console.log('Loading wasm'); this.webNodeStartTime = Date.now(); if (isBrowser()) { @@ -142,9 +143,14 @@ export class WebNodeService { return throwError(() => new Error(error.message)); }), switchMap(() => this.webnode$.asObservable()), + // filter(() => CONFIG.globalConfig.heartbeats), switchMap(() => timer(0, 60000)), switchMap(() => this.heartBeat$), switchMap(heartBeat => this.firestore.addHeartbeat(heartBeat)), + catchError(error => { + console.log('Error from heartbeat api:', error); + return of(null); + }), ); } return EMPTY; diff --git a/frontend/src/app/features/block-production/won-slots/block-production-won-slots.effects.ts b/frontend/src/app/features/block-production/won-slots/block-production-won-slots.effects.ts index 68f2412866..71b703704f 100644 --- a/frontend/src/app/features/block-production/won-slots/block-production-won-slots.effects.ts +++ b/frontend/src/app/features/block-production/won-slots/block-production-won-slots.effects.ts @@ -39,7 +39,7 @@ export class BlockProductionWonSlotsEffects extends BaseEffect { ofType(BlockProductionWonSlotsActions.getSlots, BlockProductionWonSlotsActions.close), this.latestActionState(), switchMap(({ action, state }) => - action.type === BlockProductionWonSlotsActions.close.type + action.type.includes('Close') ? EMPTY : this.wonSlotsService.getSlots().pipe( switchMap(({ slots, epoch }) => { diff --git a/frontend/src/app/features/block-production/won-slots/cards/block-production-won-slots-cards.component.html b/frontend/src/app/features/block-production/won-slots/cards/block-production-won-slots-cards.component.html index bb13a67b4d..d8e245136a 100644 --- a/frontend/src/app/features/block-production/won-slots/cards/block-production-won-slots-cards.component.html +++ b/frontend/src/app/features/block-production/won-slots/cards/block-production-won-slots-cards.component.html @@ -26,10 +26,13 @@ class="mr-10">
Public Key
-
{{ card5.publicKey | truncateMid: 6: 6 }}
-
- content_copy - Copy +
+ {{ card5.publicKey | truncateMid: 4: 4 }} + content_copy +
+
+ open_in_new + Open in Explorer
diff --git a/frontend/src/app/features/block-production/won-slots/cards/block-production-won-slots-cards.component.ts b/frontend/src/app/features/block-production/won-slots/cards/block-production-won-slots-cards.component.ts index 24b339574b..91ae264521 100644 --- a/frontend/src/app/features/block-production/won-slots/cards/block-production-won-slots-cards.component.ts +++ b/frontend/src/app/features/block-production/won-slots/cards/block-production-won-slots-cards.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { StoreDispatcher } from '@shared/base-classes/store-dispatcher.class'; import { BlockProductionWonSlotsSelectors } from '@block-production/won-slots/block-production-won-slots.state'; -import { lastItem, ONE_BILLION, ONE_THOUSAND } from '@openmina/shared'; +import { lastItem, ONE_BILLION, ONE_THOUSAND, safelyExecuteInBrowser } from '@openmina/shared'; import { getTimeDiff } from '@shared/helpers/date.helper'; import { filter } from 'rxjs'; import { @@ -12,6 +12,8 @@ import { BlockProductionWonSlotsEpoch, } from '@shared/types/block-production/won-slots/block-production-won-slots-epoch.type'; import { BlockProductionWonSlotsActions } from '@block-production/won-slots/block-production-won-slots.actions'; +import { AppSelectors } from '@app/app.state'; +import { AppNodeDetails } from '@shared/types/app/app-node-details.type'; @Component({ selector: 'mina-block-production-won-slots-cards', @@ -28,9 +30,18 @@ export class BlockProductionWonSlotsCardsComponent extends StoreDispatcher imple card4: { epochProgress: string; endIn: string; } = { epochProgress: '-', endIn: null }; card5: { publicKey: string; totalRewards: string } = { publicKey: null, totalRewards: null }; + private minaExplorer: string; + ngOnInit(): void { this.listenToSlots(); this.listenToEpoch(); + this.listenToActiveNode(); + } + + private listenToActiveNode(): void { + this.select(AppSelectors.activeNodeDetails, (node: AppNodeDetails) => { + this.minaExplorer = node.network?.toLowerCase(); + }, filter(Boolean)); } private listenToEpoch(): void { @@ -80,4 +91,10 @@ export class BlockProductionWonSlotsCardsComponent extends StoreDispatcher imple toggleSidePanel(): void { this.dispatch2(BlockProductionWonSlotsActions.toggleSidePanel()); } + + openInExplorer(): void { + const network = this.minaExplorer !== 'mainnet' ? (this.minaExplorer + '.') : ''; + const url = `https://${network}minaexplorer.com/wallet/${this.card5.publicKey}`; + safelyExecuteInBrowser(() => window.open(url, '_blank')); + } } diff --git a/frontend/src/app/features/block-production/won-slots/side-panel/block-production-won-slots-side-panel.component.ts b/frontend/src/app/features/block-production/won-slots/side-panel/block-production-won-slots-side-panel.component.ts index 486440ef00..de0a10af33 100644 --- a/frontend/src/app/features/block-production/won-slots/side-panel/block-production-won-slots-side-panel.component.ts +++ b/frontend/src/app/features/block-production/won-slots/side-panel/block-production-won-slots-side-panel.component.ts @@ -136,7 +136,6 @@ export class BlockProductionWonSlotsSidePanelComponent extends StoreDispatcher i this.stopTimer = true; this.stateWhenReachedZero = { globalSlot: this.slot.globalSlot, status: this.slot.status }; this.remainingTime = '-'; - this.queryServerOftenToGetTheNewSlotState(); } this.detect(); } else { @@ -176,16 +175,6 @@ export class BlockProductionWonSlotsSidePanelComponent extends StoreDispatcher i } } - private queryServerOftenToGetTheNewSlotState(): void { - const timer = setInterval(() => { - if (!this.stateWhenReachedZero) { - clearInterval(timer); - return; - } - this.dispatch2(BlockProductionWonSlotsActions.getSlots()); - }, 1000); - } - closeSidePanel(): void { this.router.navigate([Routes.BLOCK_PRODUCTION, Routes.WON_SLOTS]); this.dispatch2(BlockProductionWonSlotsActions.toggleSidePanel()); diff --git a/frontend/src/app/features/dashboard/dashboard-blocks-sync/dashboard-blocks-sync.component.ts b/frontend/src/app/features/dashboard/dashboard-blocks-sync/dashboard-blocks-sync.component.ts index b044d6c810..5e19b77f0e 100644 --- a/frontend/src/app/features/dashboard/dashboard-blocks-sync/dashboard-blocks-sync.component.ts +++ b/frontend/src/app/features/dashboard/dashboard-blocks-sync/dashboard-blocks-sync.component.ts @@ -128,7 +128,6 @@ export class DashboardBlocksSyncComponent extends StoreDispatcher implements OnI blocks = blocks.slice(1); this.lengthWithoutRoot = blocks.length ?? null; // 290 or 291 - console.log(this.lengthWithoutRoot); this.fetched = blocks.filter(b => ![NodesOverviewNodeBlockStatus.MISSING, NodesOverviewNodeBlockStatus.FETCHING].includes(b.status)).length; this.applied = blocks.filter(b => b.status === NodesOverviewNodeBlockStatus.APPLIED).length; diff --git a/frontend/src/app/features/leaderboard/leaderboard-apply/leaderboard-apply.component.html b/frontend/src/app/features/leaderboard/leaderboard-apply/leaderboard-apply.component.html index e5e3da605c..fb849cc486 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-apply/leaderboard-apply.component.html +++ b/frontend/src/app/features/leaderboard/leaderboard-apply/leaderboard-apply.component.html @@ -1,3 +1,3 @@ - - Round 1 Applications Close in 5d 5h 12m - Apply arrow_right_alt + + Round 1 Applications Close Soon arrow_right_alt diff --git a/frontend/src/app/features/leaderboard/leaderboard-apply/leaderboard-apply.component.scss b/frontend/src/app/features/leaderboard/leaderboard-apply/leaderboard-apply.component.scss index 447df2fdcb..97e0746e11 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-apply/leaderboard-apply.component.scss +++ b/frontend/src/app/features/leaderboard/leaderboard-apply/leaderboard-apply.component.scss @@ -1,5 +1,14 @@ @import 'leaderboard-variables'; +@keyframes bounceLeftToRight { + 0%, 100% { + transform: translateX(0); + } + 50% { + transform: translateX(10px); + } +} + .gradient { height: 52px; background: $mina-brand-gradient; @@ -13,4 +22,8 @@ @media (max-width: 767px) { font-size: 3.1vw; } + + &:hover .mina-icon { + animation: bounceLeftToRight .85s infinite ease-out; + } } diff --git a/frontend/src/app/features/leaderboard/leaderboard-details/leaderboard-details.component.scss b/frontend/src/app/features/leaderboard/leaderboard-details/leaderboard-details.component.scss index b5c5a9483d..ca5b5b1ddb 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-details/leaderboard-details.component.scss +++ b/frontend/src/app/features/leaderboard/leaderboard-details/leaderboard-details.component.scss @@ -36,6 +36,6 @@ h1 { margin-bottom: 80px; color: $mina-base-primary; font-size: 80px; - font-weight: 400; + font-weight: 300; line-height: 80px; } diff --git a/frontend/src/app/features/leaderboard/leaderboard-footer/leaderboard-footer.component.html b/frontend/src/app/features/leaderboard/leaderboard-footer/leaderboard-footer.component.html index cffccd6d15..b0ce6d1876 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-footer/leaderboard-footer.component.html +++ b/frontend/src/app/features/leaderboard/leaderboard-footer/leaderboard-footer.component.html @@ -2,7 +2,7 @@
© 2025 Mina Foundation. All rights reserved.
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 2e93e9e1d0..0f448f9ca0 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 @@ -8,6 +8,6 @@ (clickOutside)="closeMenu()"> Mina Web Node Leaderboard - Round 1 Details + Round 1 Details diff --git a/frontend/src/app/features/leaderboard/leaderboard-impressum/leaderboard-impressum.component.scss b/frontend/src/app/features/leaderboard/leaderboard-impressum/leaderboard-impressum.component.scss index b5c5a9483d..ca5b5b1ddb 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-impressum/leaderboard-impressum.component.scss +++ b/frontend/src/app/features/leaderboard/leaderboard-impressum/leaderboard-impressum.component.scss @@ -36,6 +36,6 @@ h1 { margin-bottom: 80px; color: $mina-base-primary; font-size: 80px; - font-weight: 400; + font-weight: 300; line-height: 80px; } diff --git a/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.html b/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.html index c5a364c04b..043b0cf0ea 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.html +++ b/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.html @@ -1,5 +1,9 @@ - -
+
+ +
+ +
+
@@ -10,10 +14,10 @@
-

We(b) Node
Do You?

+

We Node
Do You?

- Apply to be a node runner + Apply Now

Round 1 is limited to 100 seats

@@ -44,7 +48,7 @@

Node-To-Earn

[style.background-image]="'url(assets/images/landing-page/cta-section-bg.png)'">

Run a web node on Testnet and enter a 1000 MINA lottery

- Start Testing & Earn $500 USD + Apply Now

Apply by DATE. Not Selected? You're first in line next time.

@@ -56,13 +60,14 @@

Run a web node on Testnet and enter a 1000 MINA lottery

-

The Mina Web Node, part 1

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore . -

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore .

+

Join the Mina Web Node Testing Program

+

Discover all the details about Mina's Web Node Testing Program. Learn how to apply, participate, and earn rewards in Mina Web Node Testing Round + 1.

diff --git a/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.scss b/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.scss index 0ee7917bcf..63939df9ae 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.scss +++ b/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.scss @@ -4,12 +4,26 @@ :host { - padding-top: 52px; background-color: $mina-cta-primary; color: $mina-base-primary; font-family: "IBM Plex Sans", sans-serif; } +.floating-banner { + position: fixed; + bottom: -100%; + left: 20px; + right: 20px; + transition: bottom 0.5s ease; + z-index: 1000; + border-radius: 12px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + + &.show { + bottom: 20px; + } +} + main, mina-leaderboard-header, mina-leaderboard-footer { @@ -34,6 +48,7 @@ mina-leaderboard-footer { line-height: 20px; font-weight: 300; transition: .15s ease; + min-width: 240px; &:hover { background-color: $black; @@ -49,6 +64,7 @@ mina-leaderboard-footer { } .overflow-y-scroll { + padding-bottom: 72px; background-color: $mina-cta-primary; &::-webkit-scrollbar-track { diff --git a/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.ts b/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.ts index afce1668e7..233518cf56 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.ts +++ b/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.ts @@ -1,4 +1,7 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, DestroyRef, ElementRef, ViewChild } from '@angular/core'; +import { ManualDetection } from '@openmina/shared'; +import { debounceTime, fromEvent } from 'rxjs'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'mina-leaderboard-landing-page', @@ -7,9 +10,34 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'flex-column h-100 align-center' }, }) -export class LeaderboardLandingPageComponent implements OnInit { +export class LeaderboardLandingPageComponent extends ManualDetection implements AfterViewInit { + showBanner: boolean = false; - ngOnInit(): void { + private readonly SCROLL_THRESHOLD = 100; + @ViewChild('scrollContainer') private scrollContainer!: ElementRef; + + constructor(private destroyRef: DestroyRef) { + super(); } + ngAfterViewInit(): void { + const container = this.scrollContainer.nativeElement; + + fromEvent(container, 'scroll') + .pipe( + debounceTime(100), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(() => { + const scrollPosition = container.scrollTop; + + if (scrollPosition > this.SCROLL_THRESHOLD && !this.showBanner) { + this.showBanner = true; + this.detect(); + } else if (scrollPosition <= this.SCROLL_THRESHOLD && this.showBanner) { + this.showBanner = false; + this.detect(); + } + }); + } } 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 dcdc8794c2..cd5acc0ee7 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 @@ -1,11 +1,36 @@ - +
+ +
-
+
+
+
+ + +
+
+
+ info + Live results are not final because blockchain finality takes time +
+ chevron_right +
+
+
    +
  • New blocks can reorganize the chain, changing past data
  • +
  • Network nodes need time to reach consensus
  • +
  • Block confirmations become more certain over time
  • +
  • Final results will be published after the program ends and complete chain verification
  • +
+

To learn more about how uptime is tracked, please refer to the How Uptime Tracking Works section in the program details.

+
+
+
diff --git a/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.scss b/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.scss index 557f63b4eb..5e7f19ef29 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.scss +++ b/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.scss @@ -1,10 +1,41 @@ @import 'leaderboard-variables'; :host { - padding-top: 52px; background-color: $mina-cta-primary; } +.floating-banner { + position: fixed; + bottom: -100%; + left: 20px; + right: 20px; + transition: bottom 0.5s ease; + z-index: 1000; + border-radius: 12px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + + &.show { + bottom: 20px; + } +} + +.overflow-y-scroll { + padding-bottom: 72px; + background-color: $mina-cta-primary; + + &::-webkit-scrollbar-track { + background-color: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: $white4; + } + + &::-webkit-scrollbar-thumb:hover { + background-color: $mina-base-secondary; + } +} + main, mina-leaderboard-header, mina-leaderboard-footer { @@ -41,3 +72,17 @@ main { background-color: $mina-base-secondary; } } + +.accordion { + color: $mina-base-primary; + font-size: 16px; + font-weight: 600; + background: rgba(248, 214, 17, 0.10); + cursor: pointer; + padding: 16px; +} + +.accordion-content { + font-weight: 400; + line-height: 24px; +} 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 15e87823ce..db10f03c3f 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 @@ -1,8 +1,11 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, DestroyRef, ElementRef, OnInit, ViewChild } from '@angular/core'; import { StoreDispatcher } from '@shared/base-classes/store-dispatcher.class'; import { LeaderboardActions } from '@leaderboard/leaderboard.actions'; -import { timer } from 'rxjs'; +import { debounceTime, fromEvent, timer } from 'rxjs'; import { untilDestroyed } from '@ngneat/until-destroy'; +import { trigger, state, style, animate, transition } from '@angular/animations'; +import { ManualDetection } from '@openmina/shared'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'mina-leaderboard-page', @@ -10,8 +13,40 @@ import { untilDestroyed } from '@ngneat/until-destroy'; styleUrl: './leaderboard-page.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'flex-column h-100' }, + animations: [ + trigger('expandCollapse', [ + state('false', style({ + height: '0', + overflow: 'hidden', + opacity: '0', + })), + state('true', style({ + height: '*', + opacity: '1', + })), + transition('false <=> true', [ + animate('200ms ease-in-out'), + ]), + ]), + trigger('rotateIcon', [ + state('false', style({ transform: 'rotate(0)' })), + state('true', style({ transform: 'rotate(90deg)' })), + transition('false <=> true', [ + animate('200ms'), + ]), + ]), + ], }) -export class LeaderboardPageComponent extends StoreDispatcher implements OnInit { +export class LeaderboardPageComponent extends StoreDispatcher implements OnInit, AfterViewInit { + isExpanded = false; + showBanner: boolean = false; + + private readonly SCROLL_THRESHOLD = 100; + @ViewChild('scrollContainer') private scrollContainer!: ElementRef; + + constructor(private destroyRef: DestroyRef) { + super(); + } ngOnInit(): void { timer(0, 5000) @@ -21,4 +56,24 @@ export class LeaderboardPageComponent extends StoreDispatcher implements OnInit }); } + ngAfterViewInit(): void { + const container = this.scrollContainer.nativeElement; + + fromEvent(container, 'scroll') + .pipe( + debounceTime(100), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(() => { + const scrollPosition = container.scrollTop; + + if (scrollPosition > this.SCROLL_THRESHOLD && !this.showBanner) { + this.showBanner = true; + this.detect(); + } else if (scrollPosition <= this.SCROLL_THRESHOLD && this.showBanner) { + this.showBanner = false; + this.detect(); + } + }); + } } diff --git a/frontend/src/app/features/leaderboard/leaderboard-privacy-policy/leaderboard-privacy-policy.component.scss b/frontend/src/app/features/leaderboard/leaderboard-privacy-policy/leaderboard-privacy-policy.component.scss index b5c5a9483d..ca5b5b1ddb 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-privacy-policy/leaderboard-privacy-policy.component.scss +++ b/frontend/src/app/features/leaderboard/leaderboard-privacy-policy/leaderboard-privacy-policy.component.scss @@ -36,6 +36,6 @@ h1 { margin-bottom: 80px; color: $mina-base-primary; font-size: 80px; - font-weight: 400; + font-weight: 300; line-height: 80px; } diff --git a/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.html b/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.html index 6c9f2f3686..054a40ce1b 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.html +++ b/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.html @@ -14,7 +14,7 @@ circle {{ row.publicKey | truncateMid: (desktop ? 15 : 6): 6 }} - + {{ row.uptimePercentage }}% @if (row.uptimePercentage > 33.33) { bookmark_check diff --git a/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.scss b/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.scss index a4868d0b6c..0b6ca4bff0 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.scss +++ b/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.scss @@ -36,13 +36,20 @@ font-size: 16px; @media (max-width: 480px) { - grid-template-columns: 48% 24% 1fr; + grid-template-columns: 43% 28% 1fr; + } + + @media (max-width: 676px) { + grid-template-columns: 43% 28% 1fr; } span { color: $black; &:not(.mina-icon) { + @media (max-width: 676px) { + font-size: 2.5vw; + } @media (max-width: 480px) { font-size: 3vw; } @@ -50,13 +57,16 @@ } .circle { - color: $black4; + color: $mina-brand-gray; } .perc { - width: 37px; + width: 58px; + @media (max-width: 676px) { + width: 55px; + } @media (max-width: 480px) { - width: 26px; + width: 48px; } } diff --git a/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.ts b/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.ts index a36b6fcc7a..898136a931 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.ts +++ b/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { LeaderboardSelectors } from '@leaderboard/leaderboard.state'; import { HeartbeatSummary } from '@shared/types/leaderboard/heartbeat-summary.type'; import { StoreDispatcher } from '@shared/base-classes/store-dispatcher.class'; -import { isDesktop } from '@openmina/shared'; +import { isDesktop, TooltipPosition } from '@openmina/shared'; import { animate, style, transition, trigger } from '@angular/animations'; @Component({ @@ -47,4 +47,6 @@ export class LeaderboardTableComponent extends StoreDispatcher implements OnInit this.detect(); }); } + + protected readonly TooltipPosition = TooltipPosition; } diff --git a/frontend/src/app/features/leaderboard/leaderboard-terms-and-conditions/leaderboard-terms-and-conditions.component.scss b/frontend/src/app/features/leaderboard/leaderboard-terms-and-conditions/leaderboard-terms-and-conditions.component.scss index b5c5a9483d..ca5b5b1ddb 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-terms-and-conditions/leaderboard-terms-and-conditions.component.scss +++ b/frontend/src/app/features/leaderboard/leaderboard-terms-and-conditions/leaderboard-terms-and-conditions.component.scss @@ -36,6 +36,6 @@ h1 { margin-bottom: 80px; color: $mina-base-primary; font-size: 80px; - font-weight: 400; + font-weight: 300; line-height: 80px; } diff --git a/frontend/src/app/features/leaderboard/leaderboard-title/leaderboard-title.component.html b/frontend/src/app/features/leaderboard/leaderboard-title/leaderboard-title.component.html index 243f8138fe..7ce680fc1d 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-title/leaderboard-title.component.html +++ b/frontend/src/app/features/leaderboard/leaderboard-title/leaderboard-title.component.html @@ -1 +1,2 @@ +

Mina Web Node Testing Program

Leaderboard

diff --git a/frontend/src/app/features/leaderboard/leaderboard-title/leaderboard-title.component.scss b/frontend/src/app/features/leaderboard/leaderboard-title/leaderboard-title.component.scss index e4872bf88f..bcca6c68f7 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-title/leaderboard-title.component.scss +++ b/frontend/src/app/features/leaderboard/leaderboard-title/leaderboard-title.component.scss @@ -11,9 +11,19 @@ h1 { font-weight: 300; font-size: 80px; color: $black6; - margin: 80px 0; + margin-bottom: 80px; + margin-top: 0; @media (max-width: 1023px) { font-size: 10vw; } } + +h2 { + margin-top: 72px; + margin-bottom: 16px; + color: $mina-base-secondary; + font-size: 20px; + font-weight: 500; + line-height: 28px; +} diff --git a/frontend/src/app/features/leaderboard/leaderboard.service.ts b/frontend/src/app/features/leaderboard/leaderboard.service.ts index 6c19937356..e9baaadf2f 100644 --- a/frontend/src/app/features/leaderboard/leaderboard.service.ts +++ b/frontend/src/app/features/leaderboard/leaderboard.service.ts @@ -2,6 +2,8 @@ import { Injectable } from '@angular/core'; import { combineLatest, map, Observable } from 'rxjs'; import { HeartbeatSummary } from '@shared/types/leaderboard/heartbeat-summary.type'; import { collection, collectionData, CollectionReference, Firestore } from '@angular/fire/firestore'; +import { WebNodeService } from '@core/services/web-node.service'; +import { getElapsedTimeInMinsAndHours } from '@shared/helpers/date.helper'; @Injectable({ providedIn: 'root', @@ -11,7 +13,8 @@ export class LeaderboardService { private scoresCollection: CollectionReference; private maxScoreCollection: CollectionReference; - constructor(private firestore: Firestore) { + constructor(private firestore: Firestore, + private webnodeService: WebNodeService) { this.scoresCollection = collection(this.firestore, 'scores'); this.maxScoreCollection = collection(this.firestore, 'maxScore'); } @@ -24,14 +27,18 @@ export class LeaderboardService { map(([scores, maxScore]) => { const maxScoreRightNow = maxScore.find(c => c.id === 'current')['value']; - const items = scores.map(score => ({ - publicKey: score['publicKey'], - blocksProduced: score['blocksProduced'], - isActive: score['lastUpdated'] > Date.now() - 120000, - uptimePercentage: Math.floor((score['score'] / maxScoreRightNow) * 100), - uptimePrize: false, - blocksPrize: false, - } as HeartbeatSummary)); + const items = scores.map(score => { + return ({ + publicKey: score['publicKey'], + blocksProduced: score['blocksProduced'], + isActive: score['lastUpdated'] > Date.now() - 120000, + uptimePercentage: this.getUptimePercentage(score['score'], maxScoreRightNow), + uptimePrize: false, + blocksPrize: false, + score: score['score'], + maxScore: maxScoreRightNow, + } as HeartbeatSummary); + }); const sortedItemsByUptime = [...items].sort((a, b) => b.uptimePercentage - a.uptimePercentage); const fifthPlacePercentageByUptime = sortedItemsByUptime[4]?.uptimePercentage ?? 0; @@ -44,4 +51,30 @@ export class LeaderboardService { }), ); } + + getUptime(): Observable { + const publicKey = this.webnodeService.privateStake.publicKey.replace('\n', ''); + + return combineLatest([ + collectionData(this.scoresCollection, { idField: 'id' }), + collectionData(this.maxScoreCollection, { idField: 'id' }), + ]).pipe( + map(([scores, maxScore]) => { + const activeEntry = scores.find(score => score['publicKey'] === publicKey); + + return { + uptimePercentage: this.getUptimePercentage(activeEntry['score'], maxScore[0]['value']), + uptimeTime: getElapsedTimeInMinsAndHours(activeEntry['score'] * 5), + }; + }), + ); + } + + private getUptimePercentage(score: number, maxScore: number): number { + let uptimePercentage = Number(((score / maxScore) * 100).toFixed(2)); + if (maxScore === 0) { + uptimePercentage = 0; + } + return uptimePercentage; + } } diff --git a/frontend/src/app/features/web-node/web-node-file-upload/web-node-file-upload.component.html b/frontend/src/app/features/web-node/web-node-file-upload/web-node-file-upload.component.html index 0d5208f617..a072a18d09 100644 --- a/frontend/src/app/features/web-node/web-node-file-upload/web-node-file-upload.component.html +++ b/frontend/src/app/features/web-node/web-node-file-upload/web-node-file-upload.component.html @@ -9,7 +9,7 @@

Set Up Your Web Node

@if (!validFiles) { @if (!error) { -
+
@@ -22,7 +22,7 @@

Set Up Your Web Node

Select configuration file (.zip)
- @@ -31,7 +31,7 @@

Set Up Your Web Node

(change)="onFileSelected($event)" accept=".zip">
-
Upload webnode-account-XY.zip we sent you
+
Upload webnode-account-XY.zip we sent you
} @else {
@@ -68,18 +68,17 @@

Set Up Your Web Node

}
- - + + + + + + + +
diff --git a/frontend/src/app/features/web-node/web-node-file-upload/web-node-file-upload.component.ts b/frontend/src/app/features/web-node/web-node-file-upload/web-node-file-upload.component.ts index b8425a6323..a52f0e9f93 100644 --- a/frontend/src/app/features/web-node/web-node-file-upload/web-node-file-upload.component.ts +++ b/frontend/src/app/features/web-node/web-node-file-upload/web-node-file-upload.component.ts @@ -54,7 +54,7 @@ export class WebNodeFileUploadComponent extends ManualDetection { const publicKey = files.find(f => f.name.includes('.pub'))?.content; const password = files.find(f => f.name.includes('password'))?.content.replace(/\r?\n|\r/g, ''); const stake = files.find(f => f.name.includes('stake') && !f.name.includes('.pub'))?.content; - if (this.error || !publicKey || !password || !stake) { + if (this.error || !publicKey || !stake) { this.error = true; } else { this.webnodeService.privateStake = { publicKey, password, stake: JSON.parse(stake) }; diff --git a/frontend/src/app/layout/server-status/server-status.component.html b/frontend/src/app/layout/server-status/server-status.component.html index 0198d5a15a..9cc8bd21a3 100644 --- a/frontend/src/app/layout/server-status/server-status.component.html +++ b/frontend/src/app/layout/server-status/server-status.component.html @@ -2,21 +2,25 @@
@if (!switchForbidden && !hideNodeStats && !isMobile) { -
- blur_circular -
{{ details.transactions }} Tx{{ details.transactions | plural }}
-
{{ details.snarks }} SNARK{{ details.snarks | plural }}
-
-
- language -
{{ details.peersConnected }} Peer{{ details.peersConnected | plural }}
-
+ @if (!hideTx) { +
+ blur_circular +
{{ details.transactions }} Tx{{ details.transactions | plural }}
+
{{ details.snarks }} SNARK{{ details.snarks | plural }}
+
+ } + @if (!hidePeers) { +
+ language +
{{ details.peersConnected }} Peer{{ details.peersConnected | plural }}
+
+ } }
; diff --git a/frontend/src/app/layout/toolbar/toolbar.component.html b/frontend/src/app/layout/toolbar/toolbar.component.html index 0bdd8f4ad4..cbe14e39be 100644 --- a/frontend/src/app/layout/toolbar/toolbar.component.html +++ b/frontend/src/app/layout/toolbar/toolbar.component.html @@ -10,10 +10,13 @@ }
-
- @if (!isMobile || (isMobile && errors.length)) { +
+ @if (!isMobile) { } + @if (showUptime) { + + }
@if (haveNextBP && !isAllNodesPage) { diff --git a/frontend/src/app/layout/toolbar/toolbar.component.scss b/frontend/src/app/layout/toolbar/toolbar.component.scss index 71c8f6be53..fab767af7d 100644 --- a/frontend/src/app/layout/toolbar/toolbar.component.scss +++ b/frontend/src/app/layout/toolbar/toolbar.component.scss @@ -4,6 +4,9 @@ height: 40px; @media (max-width: 767px) { height: 96px; + &.uptime { + height: 130px; + } } } @@ -50,6 +53,13 @@ } } } + + .pills-holder { + &.is-mobile { + width: 100%; + flex-direction: column !important; + } + } } @keyframes loading { diff --git a/frontend/src/app/layout/toolbar/toolbar.component.ts b/frontend/src/app/layout/toolbar/toolbar.component.ts index 4c66ec1860..8d07dfdad3 100644 --- a/frontend/src/app/layout/toolbar/toolbar.component.ts +++ b/frontend/src/app/layout/toolbar/toolbar.component.ts @@ -1,5 +1,5 @@ -import { ChangeDetectionStrategy, Component, ElementRef, OnInit, ViewChild } from '@angular/core'; -import { filter, map } from 'rxjs'; +import { ChangeDetectionStrategy, Component, ElementRef, HostBinding, OnInit, ViewChild } from '@angular/core'; +import { catchError, filter, map, of, switchMap, timer } from 'rxjs'; import { AppSelectors } from '@app/app.state'; import { getMergedRoute, hasValue, MergedRoute, removeParamsFromURL, TooltipService } from '@openmina/shared'; import { AppMenu } from '@shared/types/app/app-menu.type'; @@ -10,6 +10,9 @@ import { selectErrorPreviewErrors } from '@error-preview/error-preview.state'; import { MinaError } from '@shared/types/error-preview/mina-error.type'; import { AppNodeStatus } from '@shared/types/app/app-node-details.type'; import { Routes } from '@shared/enums/routes.enum'; +import { CONFIG } from '@shared/constants/config'; +import { LeaderboardService } from '@leaderboard/leaderboard.service'; +import { untilDestroyed } from '@ngneat/until-destroy'; @Component({ selector: 'mina-toolbar', @@ -26,6 +29,9 @@ export class ToolbarComponent extends StoreDispatcher implements OnInit { haveNextBP: boolean; isAllNodesPage: boolean; + @HostBinding('class.uptime') + showUptime: boolean = CONFIG.showLeaderboard; + @ViewChild('loadingRef') private loadingRef: ElementRef; constructor(private tooltipService: TooltipService) { super(); } diff --git a/frontend/src/app/layout/uptime-pill/uptime-pill.component.html b/frontend/src/app/layout/uptime-pill/uptime-pill.component.html new file mode 100644 index 0000000000..4aac49b5c2 --- /dev/null +++ b/frontend/src/app/layout/uptime-pill/uptime-pill.component.html @@ -0,0 +1,5 @@ +
+ beenhere +
Uptime {{ uptime.uptimePercentage }}% {{ uptime.uptimeTime }}
+
diff --git a/frontend/src/app/layout/uptime-pill/uptime-pill.component.scss b/frontend/src/app/layout/uptime-pill/uptime-pill.component.scss new file mode 100644 index 0000000000..36ace675f6 --- /dev/null +++ b/frontend/src/app/layout/uptime-pill/uptime-pill.component.scss @@ -0,0 +1,44 @@ +@import 'openmina'; + + +.chip { + gap: 4px; + background-color: $base-surface; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 6px; + background-color: $success-container; + } + + div { + color: $success-primary; + } + + span { + color: $success-secondary; + + &.mina-icon { + color: $success-primary; + } + } + + @media (max-width: 767px) { + width: 100%; + margin-bottom: 5px; + font-size: 12px; + + .mina-icon { + display: none; + } + + &.h-sm { + height: 32px !important; + } + } +} diff --git a/frontend/src/app/layout/uptime-pill/uptime-pill.component.ts b/frontend/src/app/layout/uptime-pill/uptime-pill.component.ts new file mode 100644 index 0000000000..86740d123e --- /dev/null +++ b/frontend/src/app/layout/uptime-pill/uptime-pill.component.ts @@ -0,0 +1,38 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { catchError, of, switchMap, timer } from 'rxjs'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { LeaderboardService } from '@leaderboard/leaderboard.service'; +import { ManualDetection, OpenminaEagerSharedModule } from '@openmina/shared'; + +@UntilDestroy() +@Component({ + selector: 'mina-uptime-pill', + standalone: true, + imports: [ + OpenminaEagerSharedModule, + ], + templateUrl: './uptime-pill.component.html', + styleUrl: './uptime-pill.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class UptimePillComponent extends ManualDetection implements OnInit { + + uptime: { uptimePercentage: number, uptimeTime: string } = { uptimePercentage: 0, uptimeTime: '' }; + + constructor(private leaderboardService: LeaderboardService) { super(); } + + ngOnInit(): void { + this.listenToUptime(); + } + + private listenToUptime(): void { + timer(0, 60000).pipe( + switchMap(() => this.leaderboardService.getUptime()), + catchError((err) => of({})), + untilDestroyed(this), + ).subscribe(uptime => { + this.uptime = uptime; + this.detect(); + }); + } +} diff --git a/frontend/src/app/shared/guards/landing-page.guard.ts b/frontend/src/app/shared/guards/landing-page.guard.ts new file mode 100644 index 0000000000..d381a585a7 --- /dev/null +++ b/frontend/src/app/shared/guards/landing-page.guard.ts @@ -0,0 +1,46 @@ +import { CanActivateFn, Router } from '@angular/router'; +import { inject } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { map, take } from 'rxjs/operators'; +import { CONFIG } from '@shared/constants/config'; +import { getMergedRoute } from '@openmina/shared'; +import { Routes } from '@shared/enums/routes.enum'; + +let isFirstLoad = true; + +export const landingPageGuard: CanActivateFn = (route, state) => { + + if (!isFirstLoad || !CONFIG.showWebNodeLandingPage) { + return true; + } + const router = inject(Router); + const store = inject(Store); + isFirstLoad = false; + + return store.select(getMergedRoute).pipe( + take(1), + map(route => { + if (!route) return true; + + const startsWith = (path: string) => route.url.startsWith(path); + + if ( + startsWith('/dashboard') || + startsWith('/block-production') || + startsWith('/state') || + startsWith('/mempool') || + startsWith('/loading-web-node') + ) { + return router.createUrlTree([Routes.LOADING_WEB_NODE], { + queryParamsHandling: 'preserve', + }); + } + + if (!startsWith('/') && !startsWith('/?') && !startsWith('/leaderboard')) { + return router.createUrlTree(['']); + } + + return true; + }), + ); +}; diff --git a/frontend/src/app/shared/helpers/date.helper.ts b/frontend/src/app/shared/helpers/date.helper.ts index 2dc15940dd..97243ff87a 100644 --- a/frontend/src/app/shared/helpers/date.helper.ts +++ b/frontend/src/app/shared/helpers/date.helper.ts @@ -98,3 +98,14 @@ export function getElapsedTime(timeInSeconds: number): string { return `${minutes}m ${seconds}s`; } + +export function getElapsedTimeInMinsAndHours(timeInMinutes: number): string { + if (timeInMinutes < 60) { + return `${timeInMinutes}m`; + } + + const hours = Math.floor(timeInMinutes / 60); + const minutes = timeInMinutes % 60; + + return `${hours}h ${minutes}m`; +} diff --git a/frontend/src/app/shared/types/core/environment/mina-env.type.ts b/frontend/src/app/shared/types/core/environment/mina-env.type.ts index febcec9c4b..06d4dd4ab6 100644 --- a/frontend/src/app/shared/types/core/environment/mina-env.type.ts +++ b/frontend/src/app/shared/types/core/environment/mina-env.type.ts @@ -6,6 +6,9 @@ export interface MinaEnv { hideNodeStats?: boolean; canAddNodes?: boolean; showWebNodeLandingPage?: boolean; + showLeaderboard?: boolean; + hidePeersPill?: boolean; + hideTxPill?: boolean; sentry?: { dsn: string; tracingOrigins: string[]; @@ -14,6 +17,7 @@ export interface MinaEnv { features?: FeaturesConfig; graphQL?: string; firebase?: any; + heartbeats?: boolean; }; } diff --git a/frontend/src/app/shared/types/leaderboard/heartbeat-summary.type.ts b/frontend/src/app/shared/types/leaderboard/heartbeat-summary.type.ts index a134981c2f..9993c968b9 100644 --- a/frontend/src/app/shared/types/leaderboard/heartbeat-summary.type.ts +++ b/frontend/src/app/shared/types/leaderboard/heartbeat-summary.type.ts @@ -5,4 +5,6 @@ export interface HeartbeatSummary { blocksProduced: number; uptimePrize: boolean; blocksPrize: boolean; + score: number; + maxScore: number; } diff --git a/frontend/src/assets/environments/leaderboard.js b/frontend/src/assets/environments/leaderboard.js index 904531dd35..713f1e1ef4 100644 --- a/frontend/src/assets/environments/leaderboard.js +++ b/frontend/src/assets/environments/leaderboard.js @@ -6,12 +6,14 @@ export default { production: true, canAddNodes: false, showWebNodeLandingPage: true, + showLeaderboard: true, + hidePeersPill: true, + hideTxPill: true, globalConfig: { features: { 'dashboard': [], 'block-production': ['won-slots'], 'mempool': [], - 'benchmarks': ['wallets'], 'state': ['actions'], }, firebase: { @@ -23,6 +25,7 @@ export default { appId: '1:1016673359357:web:bbd2cbf3f031756aec7594', measurementId: 'G-ENDBL923XT', }, + heartbeats: true, }, // sentry: { // dsn: 'https://69aba72a6290383494290cf285ab13b3@o4508216158584832.ingest.de.sentry.io/4508216160616528', diff --git a/frontend/src/assets/environments/webnode.js b/frontend/src/assets/environments/webnode.js index 14ed05ebc1..99848ea84f 100644 --- a/frontend/src/assets/environments/webnode.js +++ b/frontend/src/assets/environments/webnode.js @@ -10,9 +10,6 @@ export default { features: { 'dashboard': [], 'block-production': ['won-slots'], - 'mempool': [], - 'benchmarks': ['wallets'], - 'state': ['actions'], }, firebase: { 'projectId': 'openminawebnode', diff --git a/frontend/src/environments/environment.prod.ts b/frontend/src/environments/environment.prod.ts index 4325e3ef25..f1f0b8ef10 100644 --- a/frontend/src/environments/environment.prod.ts +++ b/frontend/src/environments/environment.prod.ts @@ -10,5 +10,8 @@ export const environment: Readonly = { hideToolbar: env.hideToolbar, canAddNodes: env.canAddNodes, showWebNodeLandingPage: env.showWebNodeLandingPage, + showLeaderboard: env.showLeaderboard, + hidePeersPill: env.hidePeersPill, + hideTxPill: env.hideTxPill, sentry: env.sentry, }; diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts index 27fb7cddd8..c9d3c82a02 100644 --- a/frontend/src/environments/environment.ts +++ b/frontend/src/environments/environment.ts @@ -5,6 +5,9 @@ export const environment: Readonly = { identifier: 'Dev FE', canAddNodes: true, showWebNodeLandingPage: true, + showLeaderboard: true, + hidePeersPill: true, + hideTxPill: true, globalConfig: { features: { dashboard: [], @@ -27,6 +30,7 @@ export const environment: Readonly = { appId: '1:1016673359357:web:bbd2cbf3f031756aec7594', measurementId: 'G-ENDBL923XT', }, + heartbeats: true, graphQL: 'https://adonagy.com/graphql', // graphQL: 'https://api.minascan.io/node/devnet/v1/graphql', // graphQL: 'http://65.109.105.40:5000/graphql', @@ -87,18 +91,18 @@ export const environment: Readonly = { // resources: ['memory'], // }, // }, - { - name: 'Docker 11010', - url: 'http://localhost:11010', - }, - { - name: 'Docker 11012', - url: 'http://localhost:11012', - }, - { - name: 'Docker 11014', - url: 'http://localhost:11014', - }, + // { + // name: 'Docker 11010', + // url: 'http://localhost:11010', + // }, + // { + // name: 'Docker 11012', + // url: 'http://localhost:11012', + // }, + // { + // name: 'Docker 11014', + // url: 'http://localhost:11014', + // }, // { // name: 'Producer', // url: 'http://65.109.105.40:3000', diff --git a/frontend/src/index.html b/frontend/src/index.html index e627af2266..b04ef77d1f 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -4,7 +4,7 @@ -