diff --git a/frontend/angular.json b/frontend/angular.json index 172d639578..a9fea5b127 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -48,7 +48,8 @@ ], "sourceMap": true, "styles": [ - "src/styles.scss" + "src/styles.scss", + "node_modules/aos/dist/aos.css" ], "stylePreprocessorOptions": { "includePaths": [ @@ -58,7 +59,9 @@ "src/assets/images" ] }, - "scripts": [] + "scripts": [ + "node_modules/aos/dist/aos.js" + ] }, "configurations": { "production": { diff --git a/frontend/functions/package-lock.json b/frontend/functions/package-lock.json index c6a29d8af2..63bbe4fb94 100644 --- a/frontend/functions/package-lock.json +++ b/frontend/functions/package-lock.json @@ -22,7 +22,7 @@ "typescript": "^4.9.5" }, "engines": { - "node": "18" + "node": "22" } }, "node_modules/@ampproject/remapping": { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0b6dff0be7..d4d1b4b26a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "frontend", - "version": "1.0.66", + "version": "1.0.94", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "frontend", - "version": "1.0.66", + "version": "1.0.94", "dependencies": { "@angular/animations": "^17.3.12", "@angular/cdk": "^17.3.10", @@ -31,6 +31,7 @@ "@sentry/angular": "^8.35.0", "@sentry/cli": "^2.38.2", "@sentry/tracing": "^7.114.0", + "aos": "^2.3.4", "base-x": "^5.0.0", "bs58check": "^4.0.0", "buffer": "^6.0.3", @@ -7913,6 +7914,17 @@ "node": ">= 8" } }, + "node_modules/aos": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/aos/-/aos-2.3.4.tgz", + "integrity": "sha512-zh/ahtR2yME4I51z8IttIt4lC1Nw0ktsFtmeDzID1m9naJnWXhCoARaCgNOGXb5CLy3zm+wqmRAEgMYB5E2HUw==", + "license": "MIT", + "dependencies": { + "classlist-polyfill": "^1.0.3", + "lodash.debounce": "^4.0.6", + "lodash.throttle": "^4.0.1" + } + }, "node_modules/arch": { "version": "2.2.0", "dev": true, @@ -9081,6 +9093,12 @@ "node": ">=8" } }, + "node_modules/classlist-polyfill": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/classlist-polyfill/-/classlist-polyfill-1.2.0.tgz", + "integrity": "sha512-GzIjNdcEtH4ieA2S8NmrSxv7DfEV5fmixQeyTmqmRmRJPGpRBaSnA2a0VrCjyT8iW8JjEdMbKzDotAJf+ajgaQ==", + "license": "Unlicense" + }, "node_modules/clean-stack": { "version": "2.2.0", "dev": true, @@ -13537,8 +13555,7 @@ "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, "node_modules/lodash.isfinite": { "version": "3.3.2", @@ -13552,6 +13569,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "license": "MIT", diff --git a/frontend/package.json b/frontend/package.json index 6d93ee3d43..fc81976b66 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "1.0.79", + "version": "1.0.95", "scripts": { "install:deps": "npm install", "start": "npm install && ng serve --configuration local --open", @@ -20,7 +20,8 @@ "prerender": "ng run frontend:prerender", "sentry:sourcemaps": "sentry-cli sourcemaps inject --org openmina-uv --project openmina ./dist/frontend/browser && sentry-cli sourcemaps upload --org openmina-uv --project openmina ./dist/frontend/browser", "copy-env": "cp dist/frontend/browser/assets/environments/webnode.js dist/frontend/browser/assets/environments/env.js", - "deploy": "npm run prebuild && npm run build:prod && npm run copy-env && firebase deploy" + "deploy": "npm run prebuild && npm run build:prod && npm run copy-env && firebase deploy", + "deploy:leaderboard": "npm run prebuild && npm run build:prod && cp dist/frontend/browser/assets/environments/leaderboard.js dist/frontend/browser/assets/environments/env.js && firebase deploy" }, "private": true, "dependencies": { @@ -47,6 +48,7 @@ "@sentry/angular": "^8.35.0", "@sentry/cli": "^2.38.2", "@sentry/tracing": "^7.114.0", + "aos": "^2.3.4", "base-x": "^5.0.0", "bs58check": "^4.0.0", "buffer": "^6.0.3", diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index 79a5cf0237..3b43ad7624 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -1,8 +1,11 @@ @if (showLandingPage$ | async) { - + + + } @else if (showLoadingWebNodePage$ | async) { +} @else if (showLeaderboardPage$ | async) { + } @else if (loaded) { = this.select$(AppSelectors.menu); readonly showLandingPage$: Observable = this.select$(getMergedRoute).pipe(filter(Boolean), map((route: MergedRoute) => route.url === '/' || route.url.startsWith('/?'))); readonly showLoadingWebNodePage$: Observable = this.select$(getMergedRoute).pipe(filter(Boolean), map((route: MergedRoute) => route.url.startsWith(`/${Routes.LOADING_WEB_NODE}`))); + readonly showLeaderboardPage$: Observable = this.select$(getMergedRoute).pipe(filter(Boolean), map((route: MergedRoute) => route.url.startsWith(`/${Routes.LEADERBOARD}`))); subMenusLength: number = 0; hideToolbar: boolean = CONFIG.hideToolbar; loaded: boolean; @@ -33,6 +36,8 @@ export class AppComponent extends StoreDispatcher implements OnInit { constructor(private breakpointObserver: BreakpointObserver, private router: Router, private webNodeService: WebNodeService) { + AOS.init(); + super(); safelyExecuteInBrowser(() => { if (any(window).Cypress) { @@ -55,7 +60,7 @@ export class AppComponent extends StoreDispatcher implements OnInit { () => this.initAppFunctionalities(), filter(Boolean), take(1), - filter((route: MergedRoute) => route.url !== '/' && !route.url.startsWith('/?')), + filter((route: MergedRoute) => route.url !== '/' && !route.url.startsWith('/?') && !route.url.startsWith('/leaderboard')), ); this.select( getMergedRoute, diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 4338f94e81..734d520c3d 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -42,6 +42,7 @@ import { getPerformance, providePerformance } from '@angular/fire/performance'; import { BlockProductionPillComponent } from '@app/layout/block-production-pill/block-production-pill.component'; import { MenuTabsComponent } from '@app/layout/menu-tabs/menu-tabs.component'; import { getFirestore, provideFirestore } from '@angular/fire/firestore'; +import { LeaderboardModule } from '@leaderboard/leaderboard.module'; registerLocaleData(localeFr, 'fr'); registerLocaleData(localeEn, 'en'); @@ -164,6 +165,7 @@ export class AppGlobalErrorhandler implements ErrorHandler { WebNodeLandingPageComponent, BlockProductionPillComponent, MenuTabsComponent, + LeaderboardModule, ], providers: [ THEME_PROVIDER, @@ -179,15 +181,7 @@ export class AppGlobalErrorhandler implements ErrorHandler { }, provideClientHydration(), provideHttpClient(withFetch()), - provideFirebaseApp(() => initializeApp({ - 'projectId': 'openminawebnode', - 'appId': '1:120031499786:web:9af56c50ebce25c619f1f3', - 'storageBucket': 'openminawebnode.firebasestorage.app', - 'apiKey': 'AIzaSyBreMkb5-8ANb5zL6yWKgRAk9owbDS1g9s', - 'authDomain': 'openminawebnode.firebaseapp.com', - 'messagingSenderId': '120031499786', - 'measurementId': 'G-V0ZC81T9RQ', - })), + provideFirebaseApp(() => initializeApp(CONFIG.globalConfig.firebase)), provideAnalytics(() => getAnalytics()), ScreenTrackingService, // provideAppCheck(() => { diff --git a/frontend/src/app/app.routing.ts b/frontend/src/app/app.routing.ts index 5aa69195e5..1a5d8eca6d 100644 --- a/frontend/src/app/app.routing.ts +++ b/frontend/src/app/app.routing.ts @@ -76,6 +76,10 @@ function generateRoutes(): Routes { loadChildren: () => import('@web-node/web-node.module').then(m => m.WebNodeModule), title: WEBNODE_TITLE, }, + { + path: '', + loadChildren: () => import('@leaderboard/leaderboard.module').then(m => m.LeaderboardModule), + }, ]; if (CONFIG.showWebNodeLandingPage) { routes.push({ diff --git a/frontend/src/app/app.service.ts b/frontend/src/app/app.service.ts index c052449e32..007658b11e 100644 --- a/frontend/src/app/app.service.ts +++ b/frontend/src/app/app.service.ts @@ -62,7 +62,7 @@ export class AppService { details[key] = null; } }); - this.firestoreService.addHeartbeat(details); + // this.firestoreService.addHeartbeat(details); }), ); } diff --git a/frontend/src/app/app.setup.ts b/frontend/src/app/app.setup.ts index 8f2cf84222..252fba90f8 100644 --- a/frontend/src/app/app.setup.ts +++ b/frontend/src/app/app.setup.ts @@ -38,6 +38,8 @@ import { benchmarksReducer } from '@benchmarks/benchmarks.reducer'; import { fuzzingReducer } from '@fuzzing/fuzzing.reducer'; import { FuzzingState } from '@fuzzing/fuzzing.state'; import { FuzzingAction } from '@fuzzing/fuzzing.actions'; +import { LeaderboardState } from '@leaderboard/leaderboard.state'; +import { leaderboardReducer } from '@leaderboard/leaderboard.reducer'; export interface MinaState { [APP_KEY]: AppState; @@ -53,6 +55,7 @@ export interface MinaState { snarks: SnarksState; benchmarks: BenchmarksState; fuzzing: FuzzingState; + leaderboard: LeaderboardState; } type MinaAction = @@ -80,6 +83,7 @@ export const reducers: ActionReducerMap = { snarks: snarksReducer, benchmarks: benchmarksReducer, fuzzing: fuzzingReducer, + leaderboard: leaderboardReducer, }; export const metaReducers: MetaReducer[] = []; diff --git a/frontend/src/app/core/helpers/file-progress.helper.ts b/frontend/src/app/core/helpers/file-progress.helper.ts index ebb6a2a006..890310def7 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 = 30653596; +const WASM_FILE_SIZE = 31705944; 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 eec0036ec1..e4ab475d79 100644 --- a/frontend/src/app/core/services/firestore.service.ts +++ b/frontend/src/app/core/services/firestore.service.ts @@ -10,20 +10,24 @@ import { deleteDoc, DocumentData, } from '@angular/fire/firestore'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class FirestoreService { private heartbeatCollection: CollectionReference; + private cloudFunctionUrl = 'https://us-central1-webnode-gtm-test.cloudfunctions.net/handleValidationAndStore'; - constructor(private firestore: Firestore) { + constructor(private firestore: Firestore, + private http: HttpClient) { this.heartbeatCollection = collection(this.firestore, 'heartbeat'); } - addHeartbeat(data: any): Promise { - const docRef = doc(this.heartbeatCollection); // Auto-generated ID - return setDoc(docRef, data); + addHeartbeat(data: any): Observable { + console.log('Posting to cloud function:', data); + return this.http.post(this.cloudFunctionUrl, { data }); } updateHeartbeat(id: string, data: any): Promise { diff --git a/frontend/src/app/core/services/web-node.service.ts b/frontend/src/app/core/services/web-node.service.ts index dfdc3d39c2..549afe48cb 100644 --- a/frontend/src/app/core/services/web-node.service.ts +++ b/frontend/src/app/core/services/web-node.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { BehaviorSubject, catchError, EMPTY, filter, from, fromEvent, map, merge, Observable, of, switchMap, tap, throwError } from 'rxjs'; +import { BehaviorSubject, catchError, EMPTY, filter, from, fromEvent, map, merge, Observable, of, switchMap, tap, throwError, timer } from 'rxjs'; import base from 'base-x'; import { any, isBrowser, safelyExecuteInBrowser } from '@openmina/shared'; import { HttpClient } from '@angular/common/http'; @@ -7,6 +7,9 @@ import { sendSentryEvent } from '@shared/helpers/webnode.helper'; import { DashboardPeerStatus } from '@shared/types/dashboard/dashboard.peer'; import { FileProgressHelper } from '@core/helpers/file-progress.helper'; import { CONFIG } from '@shared/constants/config'; +import firebase from 'firebase/compat'; +import FirebaseStorageError = firebase.storage.FirebaseStorageError; +import { FirestoreService } from '@core/services/firestore.service'; export interface PrivateStake { publicKey: string; @@ -33,7 +36,8 @@ export class WebNodeService { privateStake: PrivateStake; noBlockProduction: boolean = false; - constructor(private http: HttpClient) { + constructor(private http: HttpClient, + private firestore: FirestoreService) { FileProgressHelper.initDownloadProgress(); const basex = base('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'); safelyExecuteInBrowser(() => { @@ -120,6 +124,7 @@ export class WebNodeService { } })(); console.log('webnode config:', !!this.webNodeKeyPair.privateKey, this.webNodeNetwork, urls); + console.log(this.privateStake); let privateKey = this.privateStake ? [this.privateStake.stake, this.privateStake.password] : this.webNodeKeyPair.privateKey; if (this.noBlockProduction) { privateKey = null; @@ -131,13 +136,15 @@ export class WebNodeService { any(window).webnode = webnode; this.webnode$.next(webnode); this.webnodeProgress$.next('Started'); - }), catchError((error) => { sendSentryEvent('WebNode failed to start: ' + error.message); return throwError(() => new Error(error.message)); }), switchMap(() => this.webnode$.asObservable()), + switchMap(() => timer(0, 60000)), + switchMap(() => this.heartBeat$), + switchMap(heartBeat => this.firestore.addHeartbeat(heartBeat)), ); } return EMPTY; @@ -228,6 +235,13 @@ export class WebNodeService { ); } + get heartBeat$(): Observable { + return this.webnode$.asObservable().pipe( + filter(Boolean), + switchMap(webnode => from(webnode.make_heartbeat())), + ); + } + actions$(path: string): Observable { let slot: string | number = path.split('?id=')[1]; if (!isNaN(Number(slot))) { diff --git a/frontend/src/app/features/leaderboard/leaderboard-filters/leaderboard-filters.component.html b/frontend/src/app/features/leaderboard/leaderboard-filters/leaderboard-filters.component.html new file mode 100644 index 0000000000..ac68403582 --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard-filters/leaderboard-filters.component.html @@ -0,0 +1,26 @@ +
+
+
+
+
+ arrow_upward + Uptime +
+
+
+
+ arrow_upward + Block Production +
+
+
+ +
+
diff --git a/frontend/src/app/features/leaderboard/leaderboard-filters/leaderboard-filters.component.scss b/frontend/src/app/features/leaderboard/leaderboard-filters/leaderboard-filters.component.scss new file mode 100644 index 0000000000..2ac9d53612 --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard-filters/leaderboard-filters.component.scss @@ -0,0 +1,115 @@ +@import 'leaderboard-variables'; + +@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@100;200;300;400;500;600;700&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@200..1000&display=swap'); + +:host { + font-family: "IBM Plex Sans", sans-serif; + + section { + width: 100%; + + div.filters { + padding: 16px 0; + border-bottom: 1px solid $black6; + + .sort { + box-shadow: 0 0 0 0 transparent; + padding: 0 24px; + border-radius: 44px; + font-size: 16px; + transition: all 0.15s ease; + background-color: $mina-base-divider; + color: $black; + cursor: pointer; + position: relative; + overflow: hidden; + + &:hover { + box-shadow: 0 2px 4px 0 $black3; + } + + .sort-content { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + } + + .mina-icon { + opacity: 0; + transform: translateY(-100%); + transition: all 0.3s ease; + font-size: 20px; + width: 0; + + &.show { + width: 20px; + opacity: 1; + transform: translateY(0); + } + + &.flip { + transform: rotate(180deg); + } + } + + .text { + transition: all 0.3s ease; + } + + &.active { + background-color: $mina-base-primary; + color: $mina-cta-primary; + padding-left: 16px; + + .sort-content { + transform: translateX(0); + } + + .text { + margin-left: 8px; + } + } + } + + .search { + width: 150px; + height: 32px; + + .mina-icon { + color: $black; + width: 24px; + height: 24px; + } + + input { + padding-left: 32px; + border: none; + outline: none; + font-size: 16px; + + &::placeholder { + color: $mina-base-secondary; + font-size: 16px; + font-weight: 400; + } + } + } + + @media (max-width: 480px) { + .sort { + padding: 0 12px; + font-size: 3.2vw; + } + + .search input, + .search input::placeholder { + font-size: 3.3vw; + + } + } + } + } +} + diff --git a/frontend/src/app/features/leaderboard/leaderboard-filters/leaderboard-filters.component.ts b/frontend/src/app/features/leaderboard/leaderboard-filters/leaderboard-filters.component.ts new file mode 100644 index 0000000000..dae3666c25 --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard-filters/leaderboard-filters.component.ts @@ -0,0 +1,54 @@ +import { AfterViewInit, ChangeDetectionStrategy, Component, DestroyRef, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { StoreDispatcher } from '@shared/base-classes/store-dispatcher.class'; +import { SortDirection, TableSort } from '@openmina/shared'; +import { HeartbeatSummary } from '@shared/types/leaderboard/heartbeat-summary.type'; +import { LeaderboardSelectors } from '@leaderboard/leaderboard.state'; +import { LeaderboardActions } from '@leaderboard/leaderboard.actions'; +import { fromEvent } from 'rxjs'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; + +@Component({ + selector: 'mina-leaderboard-filters', + templateUrl: './leaderboard-filters.component.html', + styleUrl: './leaderboard-filters.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { class: 'flex-row flex-center w-100' }, +}) +export class LeaderboardFiltersComponent extends StoreDispatcher implements OnInit, AfterViewInit { + + protected readonly SortDirection = SortDirection; + + @ViewChild('inputElement') private inputElement: ElementRef; + + currentSort: TableSort; + + constructor(private destroyRef: DestroyRef) {super();} + + ngOnInit(): void { + this.listenToSort(); + } + + ngAfterViewInit(): void { + fromEvent(this.inputElement.nativeElement, 'keyup') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.dispatch2(LeaderboardActions.changeFilters({ filters: { search: this.inputElement.nativeElement.value } })); + }); + } + + private listenToSort(): void { + this.select(LeaderboardSelectors.sortBy, (sort: TableSort) => { + this.currentSort = sort; + this.detect(); + }); + } + + sortBy(sortBy: string): void { + const sortDirection = sortBy !== this.currentSort.sortBy + ? this.currentSort.sortDirection + : this.currentSort.sortDirection === SortDirection.ASC ? SortDirection.DSC : SortDirection.ASC; + const sort = { sortBy: sortBy as keyof HeartbeatSummary, sortDirection }; + this.dispatch2(LeaderboardActions.sort({ sort })); + } + +} 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 new file mode 100644 index 0000000000..63bc0a839f --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard-footer/leaderboard-footer.component.html @@ -0,0 +1,8 @@ +
+
© 2025 Mina Foundation. All rights reserved.
+ +
diff --git a/frontend/src/app/features/leaderboard/leaderboard-footer/leaderboard-footer.component.scss b/frontend/src/app/features/leaderboard/leaderboard-footer/leaderboard-footer.component.scss new file mode 100644 index 0000000000..f68a12943c --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard-footer/leaderboard-footer.component.scss @@ -0,0 +1,34 @@ +@import 'leaderboard-variables'; + +:host { + font-family: "IBM Plex Sans", sans-serif; + font-size: 16px; + font-weight: 400; + line-height: 24px; + color: $mina-base-primary; + + > div { + min-height: 40px; + + @media (max-width: 1023px) { + padding: 16px 24px; + line-height: 32px; + } + } +} + +.right-side { + gap: 32px; + @media (max-width: 767px) { + gap: 0; + } + + a { + cursor: pointer; + transition: .15s ease; + + &:hover { + color: $mina-access-primary; + } + } +} diff --git a/frontend/src/app/features/leaderboard/leaderboard-footer/leaderboard-footer.component.ts b/frontend/src/app/features/leaderboard/leaderboard-footer/leaderboard-footer.component.ts new file mode 100644 index 0000000000..d41ce756c0 --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard-footer/leaderboard-footer.component.ts @@ -0,0 +1,11 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'mina-leaderboard-footer', + templateUrl: './leaderboard-footer.component.html', + styleUrl: './leaderboard-footer.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LeaderboardFooterComponent { + +} 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 new file mode 100644 index 0000000000..66a6f65b40 --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard-header/leaderboard-header.component.html @@ -0,0 +1,14 @@ + diff --git a/frontend/src/app/features/leaderboard/leaderboard-header/leaderboard-header.component.scss b/frontend/src/app/features/leaderboard/leaderboard-header/leaderboard-header.component.scss new file mode 100644 index 0000000000..0940b3d5e9 --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard-header/leaderboard-header.component.scss @@ -0,0 +1,62 @@ +@import 'leaderboard-variables'; + +.menu { + height: 58px; + font-family: "IBM Plex Mono", sans-serif; + font-size: 16px; + font-weight: 400; + color: $mina-base-primary; + + @media (max-width: 1023px) { + padding: 0 12px; + } + + .dropdown-menu { + gap: 40px; + + @media (max-width: 767px) { + width: 100%; + flex-direction: column; + position: absolute; + top: 108px; + right: 0; + background: $mina-cta-primary; + border: 1px solid $mina-base-divider; + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.1); + padding: 10px; + z-index: 1000; + line-height: 60px; + gap: 0; + + a { + width: 100%; + text-align: center; + } + + &.open { + display: flex; + } + } + } + + a { + color: $mina-base-primary; + + &.active { + color: $mina-access-primary; + } + + &:hover { + color: darken($mina-access-primary, 7%); + } + } + + .hamburger-trigger { + display: none; /* Hide trigger by default */ + cursor: pointer; + + @media (max-width: 767px) { + display: block; /* Show trigger on mobile */ + } + } +} diff --git a/frontend/src/app/features/leaderboard/leaderboard-header/leaderboard-header.component.ts b/frontend/src/app/features/leaderboard/leaderboard-header/leaderboard-header.component.ts new file mode 100644 index 0000000000..4411aacf6f --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard-header/leaderboard-header.component.ts @@ -0,0 +1,57 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { StoreDispatcher } from '@shared/base-classes/store-dispatcher.class'; +import { getMergedRoute, isDesktop, MergedRoute } from '@openmina/shared'; +import { animate, state, style, transition, trigger } from '@angular/animations'; + +@Component({ + selector: 'mina-leaderboard-header', + templateUrl: './leaderboard-header.component.html', + styleUrl: './leaderboard-header.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { class: 'flex-column' }, + animations: [ + trigger('dropdownAnimation', [ + state('closed', style({ + height: '0', + opacity: '0', + overflow: 'hidden', + })), + state('open', style({ + height: '*', + opacity: '1', + })), + transition('closed => open', [ + animate('300ms ease-out'), + ]), + transition('open => closed', [ + animate('200ms ease-in'), + ]), + ]), + ], +}) +export class LeaderboardHeaderComponent extends StoreDispatcher implements OnInit { + + route: string; + isMenuOpen: boolean = isDesktop(); + + ngOnInit(): void { + this.select(getMergedRoute, (route: MergedRoute) => { + this.route = route.url; + this.detect(); + }); + } + + closeMenu(): void { + if (isDesktop()) { + return; + } + this.isMenuOpen = false; + } + + toggleMenu(): void { + if (isDesktop()) { + return; + } + this.isMenuOpen = !this.isMenuOpen; + } +} 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 new file mode 100644 index 0000000000..3a39c82699 --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.html @@ -0,0 +1,73 @@ +
+ Round 1 Applications Close in 5d 5h 12m - Apply arrow_right_alt +
+ +
+ +
+ +
+ +
+ + +
+
+

We(b) Node
Do You?

+
+
+ Apply to be a node runner +

Round 1 is limited to 100 seats

+
+
+
+ +
+
+
+ +

Don't Trust, Verify

+

Every piece of data comes from verified sources. Produce, check and confirm blocks right from your device.

+
+
+ +

Start in One Click

+

A tab in your everyday browser lets you operate and verify the blockchain. No complex setup needed.

+
+
+ +

Node-To-Earn

+

Keep a browser tab open to earn rewards while supporting a fairer blockchain system. Testing now live.

+
+
+
+

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

+
+ Start Testing & Earn $500 USD +

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

+
+
+
+ +
+
+ +
+ +
+

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 .

+
+ +
+
+
+
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 new file mode 100644 index 0000000000..b5bfc76c70 --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.scss @@ -0,0 +1,252 @@ +@import 'leaderboard-variables'; +@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@100;200;300;400;500;600;700&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@200..1000&display=swap'); + + +:host { + padding-top: 52px; + background-color: $mina-cta-primary; + color: $mina-base-primary; + font-family: "IBM Plex Sans", sans-serif; +} + +.gradient { + height: 52px; + background: $mina-brand-gradient; + top: 0; + font-family: "IBM Plex Mono", sans-serif; + font-size: 16px; + font-weight: 400; + color: $mina-base-primary; + + @media (max-width: 767px) { + font-size: 3.1vw; + } +} + +main, +mina-leaderboard-header, +mina-leaderboard-footer { + max-width: 1200px; + width: 100%; + padding: 0 48px; + margin: 0 auto; + + @media (max-width: 1023px) { + padding: 0; + } +} + +.button-description { + a { + color: $mina-cta-primary; + padding: 0 36px; + height: 62px; + background-color: $mina-base-primary; + border-radius: 44px; + font-size: 20px; + line-height: 20px; + font-weight: 300; + transition: .15s ease; + + &:hover { + background-color: $black; + } + } + + p { + color: $black; + font-size: 16px; + font-weight: 300; + margin-bottom: 80px; + } +} + +.overflow-y-scroll { + 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; + } +} + +section { + width: 100%; + max-width: 1120px; +} + +.welcome-section { + margin: 64px 0 0; + + h1 { + font-size: 80px; + line-height: 110%; + font-weight: 300; + margin-bottom: 32px; + margin-top: 0; + letter-spacing: -3px; + @media (max-width: 1023px) { + text-align: center; + font-size: 54px; + } + } + + img { + @media (min-width: 1024px) { + max-width: 729px; + } + } +} + +.cards-section { + color: $mina-base-primary; + padding: 40px 0; + gap: 32px; + background-position: -48px -48px; + background-size: calc(100% + 48px) calc(100% + 48px); + @media (max-width: 1023px) { + max-width: calc(100% - 5vw); + margin: 0 auto; + } + + .card { + background: $mina-base-container; + border-radius: 20px; + border: 1px solid #b7cdd8; + box-shadow: 0 11.614px 24.121px -4.467px rgba(183, 205, 216, 0.20); + backdrop-filter: blur(20.777027130126953px); + padding: 40px; + + img { + width: 72px; + height: 72px; + } + + h3 { + margin-top: 24px; + font-size: 20px; + font-weight: 500; + line-height: 28px; + margin-bottom: 8px; + } + + p { + text-align: center; + font-size: 16px; + font-weight: 400; + line-height: 24px; + margin: 0; + } + } +} + +.run-web-node { + height: 574px; + color: $mina-base-primary; + background-position: -48px -48px; + background-size: calc(100% + 48px) calc(100% + 48px); + + h1 { + line-height: 118%; + font-size: 54px; + text-align: center; + margin-bottom: 64px; + font-weight: 300; + max-width: 760px; + + span { + color: $black5; + } + } +} + +.mina { + padding: 80px 0; + background: $mina-brand-gradient2; + gap: 48px; + margin-top: 24px; + + > img { + width: 100%; + } + + .explanation { + padding: 48px; + min-height: 336px; + gap: 40px; + background: rgba(239, 244, 246, 0.30); + border-radius: 20px; + border: 1px solid #b7cdd8; + box-shadow: 0 11.614px 24.121px -4.467px rgba(183, 205, 216, 0.20); + backdrop-filter: blur(20.777027130126953px); + max-width: 90%; + margin: 48px auto 0; + + img { + max-height: 240.5px; + } + + @media (max-width: 1023px) { + width: 552.5px !important; + img { + max-height: unset; + max-width: 100%; + } + } + + .text { + font-family: "IBM Plex Sans", sans-serif; + + h4 { + margin-bottom: 14px; + font-size: 20px; + line-height: 28px; + margin-top: 0; + font-weight: 400; + } + + p { + font-size: 16px; + line-height: 24px; + font-weight: 300; + } + + .line { + margin-top: 8px; + margin-bottom: 14px; + } + + a { + font-weight: 300; + font-size: 20px; + gap: 4px; + color: $mina-base-primary; + position: relative; + text-decoration: none; + } + + a::after { + content: ''; + position: absolute; + width: 0; + height: 2px; + bottom: -2px; + left: 0; + background-color: $mina-base-primary; + transition: width 0.3s ease-in-out; + } + + a:hover::after { + width: 80%; + } + } + } +} 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 new file mode 100644 index 0000000000..49e2cbb529 --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.ts @@ -0,0 +1,18 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { StoreDispatcher } from '@shared/base-classes/store-dispatcher.class'; +import { LeaderboardActions } from '@leaderboard/leaderboard.actions'; + +@Component({ + selector: 'mina-leaderboard-landing-page', + templateUrl: './leaderboard-landing-page.component.html', + styleUrl: './leaderboard-landing-page.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { class: 'flex-column h-100 align-center' }, +}) +export class LeaderboardLandingPageComponent extends StoreDispatcher implements OnInit { + + ngOnInit(): void { + this.dispatch2(LeaderboardActions.getHeartbeats()); + } + +} 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 new file mode 100644 index 0000000000..75a4bf0380 --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.html @@ -0,0 +1,13 @@ +
+ Round 1 Applications Close in 5d 5h 12m - Apply arrow_right_alt +
+ +
+ +
+ + + +
+ +
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 new file mode 100644 index 0000000000..3badebe741 --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.scss @@ -0,0 +1,57 @@ +@import 'leaderboard-variables'; + +:host { + padding-top: 52px; + background-color: $mina-cta-primary; +} + +.gradient { + height: 52px; + background: $mina-brand-gradient; + top: 0; + font-family: "IBM Plex Mono", sans-serif; + font-size: 16px; + font-weight: 400; + color: $mina-base-primary; + + @media (max-width: 767px) { + font-size: 3.1vw; + } +} + +main, +mina-leaderboard-header, +mina-leaderboard-footer { + max-width: 1200px; + width: 100%; + padding: 0 48px; + margin: 0 auto; + + @media (max-width: 1023px) { + padding: 0; + } +} + +main { + border-bottom: 1px solid $mina-base-divider; + + @media (max-width: 1023px) { + padding: 0 10px; + } +} + +.overflow-auto { + 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; + } +} 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 new file mode 100644 index 0000000000..c5a6f20c2d --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.ts @@ -0,0 +1,18 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { StoreDispatcher } from '@shared/base-classes/store-dispatcher.class'; +import { LeaderboardActions } from '@leaderboard/leaderboard.actions'; + +@Component({ + selector: 'mina-leaderboard-page', + templateUrl: './leaderboard-page.component.html', + styleUrl: './leaderboard-page.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { class: 'flex-column h-100' }, +}) +export class LeaderboardPageComponent extends StoreDispatcher implements OnInit { + + ngOnInit(): void { + this.dispatch2(LeaderboardActions.getHeartbeats()); + } + +} 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 new file mode 100644 index 0000000000..d225b8fa1c --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.html @@ -0,0 +1,24 @@ +
+
+ Web Node Public Key + Uptime + Produced Blocks +
+
+
+ @for (row of rows; track $index) { +
+
+ + circle + {{ row.publicKey | truncateMid: (desktop ? 15 : 6): 6 }} + + + {{ row.uptimePercentage }}% + bookmark_check + + {{ row.blocksProduced ?? 0 }} +
+
+ } +
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 new file mode 100644 index 0000000000..81c62669a9 --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.scss @@ -0,0 +1,79 @@ +@import 'leaderboard-variables'; + +.mina-icon { + font-variation-settings: 'FILL' 1, 'wght' 400 !important; +} + +.row.head span { + color: $black; + font-size: 16px; + font-weight: 400; +} + +.row-wrap { + + &.head { + max-width: unset; + height: 56px; + + .row { + margin: 0 auto; + font-family: "IBM Plex Sans", sans-serif; + } + } + + &:nth-child(odd):not(.head) { + background-color: $mina-base-container; + } +} + +.row { + display: grid; + grid-template-columns: 40% 20% 1fr; + width: 100%; + height: 40px; + line-height: 40px; + font-family: "IBM Plex Mono", sans-serif; + font-size: 16px; + + @media (max-width: 480px) { + grid-template-columns: 48% 24% 1fr; + } + + span { + color: $black; + + &:not(.mina-icon) { + @media (max-width: 480px) { + font-size: 3vw; + } + } + } + + .circle { + color: $black4; + } + + .perc { + width: 50px; + } + + .circle.active { + color: $mina-brand-cyan; + } +} + +.fx-row-vert-cent { + .mina-icon { + width: 26px; + } + + mina-loading-spinner { + margin-left: -5px; + margin-right: 5px; + } + + .mina-icon { + padding-left: 2px; + } +} 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 new file mode 100644 index 0000000000..e8c77a914c --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.ts @@ -0,0 +1,38 @@ +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'; + +@Component({ + selector: 'mina-leaderboard-table', + templateUrl: './leaderboard-table.component.html', + styleUrl: './leaderboard-table.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { class: 'flex-column w-100 h-100' }, +}) +export class LeaderboardTableComponent extends StoreDispatcher implements OnInit { + + isLoading: boolean; + rows: HeartbeatSummary[] = []; + desktop: boolean = isDesktop(); + + ngOnInit(): void { + this.listenToEmptyInDatabase(); + this.listenToHeartbeatsChanges(); + } + + private listenToEmptyInDatabase(): void { + this.select(LeaderboardSelectors.isLoading, (loading: boolean) => { + this.isLoading = loading; + this.detect(); + }); + } + + private listenToHeartbeatsChanges(): void { + this.select(LeaderboardSelectors.filteredHeartbeatSummaries, (rows: HeartbeatSummary[]) => { + this.rows = rows; + this.detect(); + }); + } +} 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 new file mode 100644 index 0000000000..243f8138fe --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard-title/leaderboard-title.component.html @@ -0,0 +1 @@ +

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 new file mode 100644 index 0000000000..e4872bf88f --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard-title/leaderboard-title.component.scss @@ -0,0 +1,19 @@ +@import 'leaderboard-variables'; + +:host { + max-width: 1200px; + width: 100%; + margin: 0 auto; +} + +h1 { + font-family: "IBM Plex Sans", sans-serif; + font-weight: 300; + font-size: 80px; + color: $black6; + margin: 80px 0; + + @media (max-width: 1023px) { + font-size: 10vw; + } +} diff --git a/frontend/src/app/features/leaderboard/leaderboard-title/leaderboard-title.component.ts b/frontend/src/app/features/leaderboard/leaderboard-title/leaderboard-title.component.ts new file mode 100644 index 0000000000..ab3ff4348d --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard-title/leaderboard-title.component.ts @@ -0,0 +1,27 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { StoreDispatcher } from '@shared/base-classes/store-dispatcher.class'; +import { HeartbeatSummary } from '@shared/types/leaderboard/heartbeat-summary.type'; +import { LeaderboardSelectors } from '@leaderboard/leaderboard.state'; + +@Component({ + selector: 'mina-leaderboard-title', + templateUrl: './leaderboard-title.component.html', + styleUrl: './leaderboard-title.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { class: 'flex-column' }, +}) +export class LeaderboardTitleComponent extends StoreDispatcher implements OnInit { + + rows: HeartbeatSummary[] = []; + + ngOnInit(): void { + this.listenToHeartbeatsChanges(); + } + + private listenToHeartbeatsChanges(): void { + this.select(LeaderboardSelectors.filteredHeartbeatSummaries, (rows: HeartbeatSummary[]) => { + this.rows = rows; + this.detect(); + }); + } +} diff --git a/frontend/src/app/features/leaderboard/leaderboard.actions.ts b/frontend/src/app/features/leaderboard/leaderboard.actions.ts new file mode 100644 index 0000000000..e89de21b15 --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard.actions.ts @@ -0,0 +1,27 @@ +import { createType } from '@shared/constants/store-functions'; +import { createAction, props } from '@ngrx/store'; +import { HeartbeatSummary } from '@shared/types/leaderboard/heartbeat-summary.type'; +import { TableSort } from '@openmina/shared'; + +export const LEADERBOARD_KEY = 'leaderboard'; +export const LEADERBOARD_PREFIX = 'Leaderboard'; + +const type = (type: T) => createType(LEADERBOARD_PREFIX, null, type); + +const init = createAction(type('Init')); +const close = createAction(type('Close')); +const getHeartbeats = createAction(type('Get Heartbeats')); +const getHeartbeatsSuccess = createAction(type('Get Heartbeats Success'), props<{ + heartbeatSummaries: HeartbeatSummary[], +}>()); +const changeFilters = createAction(type('Change Filters'), props<{ filters: any }>()); +const sort = createAction(type('Sort'), props<{ sort: TableSort }>()); + +export const LeaderboardActions = { + init, + close, + getHeartbeats, + getHeartbeatsSuccess, + changeFilters, + sort, +}; diff --git a/frontend/src/app/features/leaderboard/leaderboard.effects.ts b/frontend/src/app/features/leaderboard/leaderboard.effects.ts new file mode 100644 index 0000000000..bc784019b9 --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard.effects.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core'; +import { MinaState, selectMinaState } from '@app/app.setup'; +import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { Effect } from '@openmina/shared'; +import { map, switchMap } from 'rxjs'; +import { catchErrorAndRepeat2 } from '@shared/constants/store-functions'; +import { MinaErrorType } from '@shared/types/error-preview/mina-error-type.enum'; +import { Store } from '@ngrx/store'; +import { BaseEffect } from '@shared/base-classes/mina-rust-base.effect'; +import { LeaderboardActions } from '@leaderboard/leaderboard.actions'; +import { LeaderboardService } from '@leaderboard/leaderboard.service'; + +@Injectable({ + providedIn: 'root', +}) +export class LeaderboardEffects extends BaseEffect { + + readonly getHeartbeats$: Effect; + + constructor(private actions$: Actions, + private leaderboardService: LeaderboardService, + store: Store) { + super(store, selectMinaState); + + this.getHeartbeats$ = createEffect(() => this.actions$.pipe( + ofType(LeaderboardActions.getHeartbeats), + this.latestActionState(), + switchMap(() => this.leaderboardService.getHeartbeatsSummaries()), + map(heartbeatSummaries => LeaderboardActions.getHeartbeatsSuccess({ heartbeatSummaries })), + catchErrorAndRepeat2(MinaErrorType.GENERIC, LeaderboardActions.getHeartbeatsSuccess({ heartbeatSummaries: [] })), + )); + } +} diff --git a/frontend/src/app/features/leaderboard/leaderboard.module.ts b/frontend/src/app/features/leaderboard/leaderboard.module.ts new file mode 100644 index 0000000000..b44db7f664 --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard.module.ts @@ -0,0 +1,40 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { LeaderboardRouting } from './leaderboard.routing'; +import { LeaderboardFiltersComponent } from '@leaderboard/leaderboard-filters/leaderboard-filters.component'; +import { LeaderboardHeaderComponent } from '@leaderboard/leaderboard-header/leaderboard-header.component'; +import { LeaderboardPageComponent } from '@leaderboard/leaderboard-page/leaderboard-page.component'; +import { LeaderboardTableComponent } from '@leaderboard/leaderboard-table/leaderboard-table.component'; +import { LeaderboardTitleComponent } from '@leaderboard/leaderboard-title/leaderboard-title.component'; +import { CopyComponent, OpenminaSharedModule } from '@openmina/shared'; +import { LoadingSpinnerComponent } from '@shared/loading-spinner/loading-spinner.component'; +import { EffectsModule } from '@ngrx/effects'; +import { LeaderboardEffects } from '@leaderboard/leaderboard.effects'; +import { LeaderboardFooterComponent } from '@leaderboard/leaderboard-footer/leaderboard-footer.component'; +import { LeaderboardLandingPageComponent } from '@leaderboard/leaderboard-landing-page/leaderboard-landing-page.component'; + + +@NgModule({ + declarations: [ + LeaderboardPageComponent, + LeaderboardFiltersComponent, + LeaderboardHeaderComponent, + LeaderboardTableComponent, + LeaderboardTitleComponent, + LeaderboardFooterComponent, + LeaderboardLandingPageComponent, + ], + imports: [ + CommonModule, + LeaderboardRouting, + CopyComponent, + OpenminaSharedModule, + LoadingSpinnerComponent, + EffectsModule.forFeature(LeaderboardEffects), + ], + exports: [ + LeaderboardLandingPageComponent, + ], +}) +export class LeaderboardModule {} diff --git a/frontend/src/app/features/leaderboard/leaderboard.reducer.ts b/frontend/src/app/features/leaderboard/leaderboard.reducer.ts new file mode 100644 index 0000000000..770d162cb4 --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard.reducer.ts @@ -0,0 +1,56 @@ +import { createReducer, on } from '@ngrx/store'; +import { LeaderboardState } from '@leaderboard/leaderboard.state'; +import { LeaderboardActions } from '@leaderboard/leaderboard.actions'; +import { HeartbeatSummary } from '@shared/types/leaderboard/heartbeat-summary.type'; +import { sort, SortDirection, TableSort } from '@openmina/shared'; + + +const initialState: LeaderboardState = { + filteredHeartbeatSummaries: [], + heartbeatSummaries: [], + filters: { + search: '', + }, + sortBy: { + sortDirection: SortDirection.DSC, + sortBy: 'uptimePercentage', + }, + isLoading: true, +}; + +export const leaderboardReducer = createReducer( + initialState, + on(LeaderboardActions.getHeartbeatsSuccess, (state, { heartbeatSummaries }) => ({ + ...state, + isLoading: false, + heartbeatSummaries, + filteredHeartbeatSummaries: sortHeartbeats(filterHeartbeats(heartbeatSummaries, state.filters), state.sortBy), + })), + on(LeaderboardActions.changeFilters, (state, { filters }) => ({ + ...state, + filters, + filteredHeartbeatSummaries: sortHeartbeats(filterHeartbeats(state.heartbeatSummaries, filters), state.sortBy), + })), + on(LeaderboardActions.sort, (state, { sort }) => ({ + ...state, + sortBy: sort, + filteredHeartbeatSummaries: sortHeartbeats(state.filteredHeartbeatSummaries, sort), + })), +); + + +function sortHeartbeats(node: HeartbeatSummary[], tableSort: TableSort): HeartbeatSummary[] { + return sort(node, tableSort, []); +} + +function filterHeartbeats(summaries: HeartbeatSummary[], filters: any): HeartbeatSummary[] { + return summaries.filter(summary => { + if (filters.search?.length) { + const searchTerm = filters.search.toLowerCase(); + const searchMatch = summary.publicKey.toLowerCase().includes(searchTerm); + if (!searchMatch) return false; + } + + return true; + }); +} diff --git a/frontend/src/app/features/leaderboard/leaderboard.routing.ts b/frontend/src/app/features/leaderboard/leaderboard.routing.ts new file mode 100644 index 0000000000..32e605a291 --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard.routing.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { LeaderboardPageComponent } from '@leaderboard/leaderboard-page/leaderboard-page.component'; + +const routes: Routes = [ + { + path: 'leaderboard', + component: LeaderboardPageComponent, + }, + { + path: '**', + redirectTo: '', + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class LeaderboardRouting {} diff --git a/frontend/src/app/features/leaderboard/leaderboard.service.ts b/frontend/src/app/features/leaderboard/leaderboard.service.ts new file mode 100644 index 0000000000..e18eb7efbe --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard.service.ts @@ -0,0 +1,258 @@ +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { HeartbeatSummary } from '@shared/types/leaderboard/heartbeat-summary.type'; + +@Injectable({ + providedIn: 'root', +}) +export class LeaderboardService { + + constructor() { } + + getHeartbeatsSummaries(): Observable { + const mockData: HeartbeatSummary[] = [ + { + publicKey: '0x7a23c98f21345dc98e23a5b1c98d23b1c98d23b1c', + isActive: true, + uptimePercentage: 99.8, + blocksProduced: 1243, + }, + { + publicKey: '0x8b34d09e32456ed09f34b6c2d09f34b6c2d09f34', + isActive: true, + uptimePercentage: 98.2, + blocksProduced: 982, + }, + { + publicKey: '0x9c45e10f43567fe10a45c7d3e10a45c7d3e10a45', + isActive: false, + uptimePercentage: 45.6, + blocksProduced: 234, + }, + { + publicKey: '0x0d56f21g54678gf21b56d8e4f21b56d8e4f21b56', + isActive: true, + uptimePercentage: 56.9, + blocksProduced: 876, + }, + { + publicKey: '0x1e67g32h65789hg32c67e9f5g32c67e9f5g32c67', + isActive: true, + uptimePercentage: 23.1, + blocksProduced: 1102, + }, + { + publicKey: '0x7a23c98f21345dc98e23a5b1c98d23b1c98d23b1c', + isActive: true, + uptimePercentage: 99.8, + blocksProduced: 1243, + }, + { + publicKey: '0x8b34d09e32456ed09f34b6c2d09f34b6c2d09f34', + isActive: false, + uptimePercentage: 98.2, + blocksProduced: 982, + }, + { + publicKey: '0x9c45e10f43567fe10a45c7d3e10a45c7d3e10a45', + isActive: false, + uptimePercentage: 45.6, + blocksProduced: 234, + }, + { + publicKey: '0x0d56f21g54678gf21b56d8e4f21b56d8e4f21b56', + isActive: true, + uptimePercentage: 56.9, + blocksProduced: 876, + }, + { + publicKey: '0x1e67g32h65789hg32c67e9f5g32c67e9f5g32c67', + isActive: false, + uptimePercentage: 23.1, + blocksProduced: 1102, + }, + { + publicKey: '0x7a23c98f21345dc98e23a5b1c98d23b1c98d23b1c', + isActive: false, + uptimePercentage: 99.8, + blocksProduced: 1243, + }, + { + publicKey: '0x8b34d09e32456ed09f34b6c2d09f34b6c2d09f34', + isActive: false, + uptimePercentage: 98.2, + blocksProduced: 982, + }, + { + publicKey: '0x9c45e10f43567fe10a45c7d3e10a45c7d3e10a45', + isActive: false, + uptimePercentage: 45.6, + blocksProduced: 234, + }, + { + publicKey: '0x0d56f21g54678gf21b56d8e4f21b56d8e4f21b56', + isActive: true, + uptimePercentage: 56.9, + blocksProduced: 876, + }, + { + publicKey: '0x1e67g32h65789hg32c67e9f5g32c67e9f5g32c67', + isActive: true, + uptimePercentage: 23.1, + blocksProduced: 1102, + }, + { + publicKey: '0x7a23c98f21345dc98e23a5b1c98d23b1c98d23b1c', + isActive: true, + uptimePercentage: 99.8, + blocksProduced: 1243, + }, + { + publicKey: '0x8b34d09e32456ed09f34b6c2d09f34b6c2d09f34', + isActive: true, + uptimePercentage: 98.2, + blocksProduced: 982, + }, + { + publicKey: '0x9c45e10f43567fe10a45c7d3e10a45c7d3e10a45', + isActive: false, + uptimePercentage: 45.6, + blocksProduced: 234, + }, + { + publicKey: '0x0d56f21g54678gf21b56d8e4f21b56d8e4f21b56', + isActive: true, + uptimePercentage: 56.9, + blocksProduced: 876, + }, + { + publicKey: '0x1e67g32h65789hg32c67e9f5g32c67e9f5g32c67', + isActive: true, + uptimePercentage: 23.1, + blocksProduced: 1102, + }, + { + publicKey: '0x7a23c98f21345dc98e23a5b1c98d23b1c98d23b1c', + isActive: true, + uptimePercentage: 99.8, + blocksProduced: 1243, + }, + { + publicKey: '0x8b34d09e32456ed09f34b6c2d09f34b6c2d09f34', + isActive: true, + uptimePercentage: 98.2, + blocksProduced: 982, + }, + { + publicKey: '0x9c45e10f43567fe10a45c7d3e10a45c7d3e10a45', + isActive: false, + uptimePercentage: 45.6, + blocksProduced: 234, + }, + { + publicKey: '0x0d56f21g54678gf21b56d8e4f21b56d8e4f21b56', + isActive: true, + uptimePercentage: 56.9, + blocksProduced: 876, + }, + { + publicKey: '0x1e67g32h65789hg32c67e9f5g32c67e9f5g32c67', + isActive: true, + uptimePercentage: 23.1, + blocksProduced: 1102, + }, + { + publicKey: '0x7a23c98f21345dc98e23a5b1c98d23b1c98d23b1c', + isActive: true, + uptimePercentage: 99.8, + blocksProduced: 1243, + }, + { + publicKey: '0x8b34d09e32456ed09f34b6c2d09f34b6c2d09f34', + isActive: false, + uptimePercentage: 98.2, + blocksProduced: 982, + }, + { + publicKey: '0x9c45e10f43567fe10a45c7d3e10a45c7d3e10a45', + isActive: false, + uptimePercentage: 45.6, + blocksProduced: 234, + }, + { + publicKey: '0x0d56f21g54678gf21b56d8e4f21b56d8e4f21b56', + isActive: true, + uptimePercentage: 56.9, + blocksProduced: 876, + }, + { + publicKey: '0x1e67g32h65789hg32c67e9f5g32c67e9f5g32c67', + isActive: false, + uptimePercentage: 23.1, + blocksProduced: 1102, + }, + { + publicKey: '0x7a23c98f21345dc98e23a5b1c98d23b1c98d23b1c', + isActive: false, + uptimePercentage: 99.8, + blocksProduced: 1243, + }, + { + publicKey: '0x8b34d09e32456ed09f34b6c2d09f34b6c2d09f34', + isActive: false, + uptimePercentage: 98.2, + blocksProduced: 982, + }, + { + publicKey: '0x9c45e10f43567fe10a45c7d3e10a45c7d3e10a45', + isActive: false, + uptimePercentage: 45.6, + blocksProduced: 234, + }, + { + publicKey: '0x0d56f21g54678gf21b56d8e4f21b56d8e4f21b56', + isActive: true, + uptimePercentage: 56.9, + blocksProduced: 876, + }, + { + publicKey: '0x1e67g32h65789hg32c67e9f5g32c67e9f5g32c67', + isActive: true, + uptimePercentage: 23.1, + blocksProduced: 1102, + }, + { + publicKey: '0x7a23c98f21345dc98e23a5b1c98d23b1c98d23b1c', + isActive: true, + uptimePercentage: 99.8, + blocksProduced: 1243, + }, + { + publicKey: '0x8b34d09e32456ed09f34b6c2d09f34b6c2d09f34', + isActive: true, + uptimePercentage: 98.2, + blocksProduced: 982, + }, + { + publicKey: '0x9c45e10f43567fe10a45c7d3e10a45c7d3e10a45', + isActive: false, + uptimePercentage: 45.6, + blocksProduced: 234, + }, + { + publicKey: '0x0d56f21g54678gf21b56d8e4f21b56d8e4f21b56', + isActive: true, + uptimePercentage: 56.9, + blocksProduced: 876, + }, + { + publicKey: '0x1e67g32h65789hg32c67e9f5g32c67e9f5g32c67', + isActive: true, + uptimePercentage: 23.1, + blocksProduced: 1102, + }, + ]; + + return of(mockData); + } +} diff --git a/frontend/src/app/features/leaderboard/leaderboard.state.ts b/frontend/src/app/features/leaderboard/leaderboard.state.ts new file mode 100644 index 0000000000..32eb9257ff --- /dev/null +++ b/frontend/src/app/features/leaderboard/leaderboard.state.ts @@ -0,0 +1,29 @@ +import { createFeatureSelector, createSelector, MemoizedSelector } from '@ngrx/store'; +import { MinaState } from '@app/app.setup'; +import { HeartbeatSummary } from '@shared/types/leaderboard/heartbeat-summary.type'; +import { LEADERBOARD_KEY } from '@leaderboard/leaderboard.actions'; +import { TableSort } from '@openmina/shared'; + +export interface LeaderboardState { + filteredHeartbeatSummaries: HeartbeatSummary[]; + heartbeatSummaries: HeartbeatSummary[]; + filters: { search: string }; + sortBy: TableSort; + isLoading: boolean; +} + +const select = (selector: (state: LeaderboardState) => T): MemoizedSelector => createSelector( + createFeatureSelector(LEADERBOARD_KEY), + selector, +); +const filteredHeartbeatSummaries = select(state => state.filteredHeartbeatSummaries); +const filters = select(state => state.filters); +const sortBy = select(state => state.sortBy); +const isLoading = select(state => state.isLoading); + +export const LeaderboardSelectors = { + filteredHeartbeatSummaries, + filters, + sortBy, + isLoading, +}; diff --git a/frontend/src/app/features/web-node/web-node.component.ts b/frontend/src/app/features/web-node/web-node.component.ts index c522bf8efc..68de34ddd1 100644 --- a/frontend/src/app/features/web-node/web-node.component.ts +++ b/frontend/src/app/features/web-node/web-node.component.ts @@ -42,7 +42,7 @@ export class WebNodeComponent extends StoreDispatcher implements OnInit { private listenToRoute(): void { this.select(getMergedRoute, (route: MergedRoute) => { - let initial = 175; + let initial = 176; if (route.queryParams['initial']) { initial = Number(route.queryParams['initial']); } diff --git a/frontend/src/app/layout/web-node-landing-page/web-node-landing-page.component.ts b/frontend/src/app/layout/web-node-landing-page/web-node-landing-page.component.ts index f8cd2143ec..f5d129cfb9 100644 --- a/frontend/src/app/layout/web-node-landing-page/web-node-landing-page.component.ts +++ b/frontend/src/app/layout/web-node-landing-page/web-node-landing-page.component.ts @@ -1,5 +1,4 @@ import { ChangeDetectionStrategy, Component, EventEmitter, OnInit, Output } from '@angular/core'; -import { NgOptimizedImage } from '@angular/common'; import { StoreDispatcher } from '@shared/base-classes/store-dispatcher.class'; import { AppSelectors } from '@app/app.state'; import { filter } from 'rxjs'; @@ -7,9 +6,6 @@ import { filter } from 'rxjs'; @Component({ selector: 'mina-web-node-landing-page', standalone: true, - imports: [ - NgOptimizedImage, - ], templateUrl: './web-node-landing-page.component.html', styleUrl: './web-node-landing-page.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/frontend/src/app/shared/enums/routes.enum.ts b/frontend/src/app/shared/enums/routes.enum.ts index 0a30a63b14..bb7b68e769 100644 --- a/frontend/src/app/shared/enums/routes.enum.ts +++ b/frontend/src/app/shared/enums/routes.enum.ts @@ -41,4 +41,5 @@ export enum Routes { OCAML = 'ocaml', RUST = 'rust', FUZZING = 'fuzzing', + LEADERBOARD = 'leaderboard', } 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 0edf5d8720..febcec9c4b 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 @@ -13,6 +13,7 @@ export interface MinaEnv { globalConfig?: { features?: FeaturesConfig; graphQL?: string; + firebase?: any; }; } diff --git a/frontend/src/app/shared/types/leaderboard/heartbeat-summary.type.ts b/frontend/src/app/shared/types/leaderboard/heartbeat-summary.type.ts new file mode 100644 index 0000000000..db5e249ab2 --- /dev/null +++ b/frontend/src/app/shared/types/leaderboard/heartbeat-summary.type.ts @@ -0,0 +1,6 @@ +export interface HeartbeatSummary { + publicKey: string; + isActive: boolean; + uptimePercentage: number; + blocksProduced: number; +} diff --git a/frontend/src/app/shared/types/leaderboard/heartbeat.type.ts b/frontend/src/app/shared/types/leaderboard/heartbeat.type.ts new file mode 100644 index 0000000000..1e697df0e6 --- /dev/null +++ b/frontend/src/app/shared/types/leaderboard/heartbeat.type.ts @@ -0,0 +1,3 @@ +// export interface Heartbeat { +// +// } diff --git a/frontend/src/assets/environments/leaderboard.js b/frontend/src/assets/environments/leaderboard.js new file mode 100644 index 0000000000..904531dd35 --- /dev/null +++ b/frontend/src/assets/environments/leaderboard.js @@ -0,0 +1,37 @@ +/** + * This configuration is used for the staging-webnode environment. + */ + +export default { + production: true, + canAddNodes: false, + showWebNodeLandingPage: true, + globalConfig: { + features: { + 'dashboard': [], + 'block-production': ['won-slots'], + 'mempool': [], + 'benchmarks': ['wallets'], + 'state': ['actions'], + }, + firebase: { + apiKey: 'AIzaSyBZzFsHjIbQVbBP0N-KkUsEvHRVU_wwd7g', + authDomain: 'webnode-gtm-test.firebaseapp.com', + projectId: 'webnode-gtm-test', + storageBucket: 'webnode-gtm-test.firebasestorage.app', + messagingSenderId: '1016673359357', + appId: '1:1016673359357:web:bbd2cbf3f031756aec7594', + measurementId: 'G-ENDBL923XT', + }, + }, + // sentry: { + // dsn: 'https://69aba72a6290383494290cf285ab13b3@o4508216158584832.ingest.de.sentry.io/4508216160616528', + // tracingOrigins: ['https://www.openmina.com', 'webnode-gtm-test.firebaseapp.com', 'webnode-gtm-test.firebasestorage.app'], + // }, + configs: [ + { + name: 'Web Node', + isWebNode: true, + }, + ], +}; diff --git a/frontend/src/assets/environments/webnode.js b/frontend/src/assets/environments/webnode.js index 37014186fd..14ed05ebc1 100644 --- a/frontend/src/assets/environments/webnode.js +++ b/frontend/src/assets/environments/webnode.js @@ -14,6 +14,15 @@ export default { 'benchmarks': ['wallets'], 'state': ['actions'], }, + firebase: { + 'projectId': 'openminawebnode', + 'appId': '1:120031499786:web:9af56c50ebce25c619f1f3', + 'storageBucket': 'openminawebnode.firebasestorage.app', + 'apiKey': 'AIzaSyBreMkb5-8ANb5zL6yWKgRAk9owbDS1g9s', + 'authDomain': 'openminawebnode.firebaseapp.com', + 'messagingSenderId': '120031499786', + 'measurementId': 'G-V0ZC81T9RQ', + }, }, sentry: { dsn: 'https://69aba72a6290383494290cf285ab13b3@o4508216158584832.ingest.de.sentry.io/4508216160616528', diff --git a/frontend/src/assets/images/landing-page/blog-featured-image.jpg b/frontend/src/assets/images/landing-page/blog-featured-image.jpg new file mode 100644 index 0000000000..5d79cbe252 Binary files /dev/null and b/frontend/src/assets/images/landing-page/blog-featured-image.jpg differ diff --git a/frontend/src/assets/images/landing-page/cta-section-bg.png b/frontend/src/assets/images/landing-page/cta-section-bg.png new file mode 100644 index 0000000000..37642a93a6 Binary files /dev/null and b/frontend/src/assets/images/landing-page/cta-section-bg.png differ diff --git a/frontend/src/assets/images/landing-page/hero-image.jpg b/frontend/src/assets/images/landing-page/hero-image.jpg new file mode 100644 index 0000000000..6398a10d62 Binary files /dev/null and b/frontend/src/assets/images/landing-page/hero-image.jpg differ diff --git a/frontend/src/assets/images/landing-page/icon-click.svg b/frontend/src/assets/images/landing-page/icon-click.svg new file mode 100644 index 0000000000..eaf794cc9b --- /dev/null +++ b/frontend/src/assets/images/landing-page/icon-click.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/assets/images/landing-page/icon-earn.svg b/frontend/src/assets/images/landing-page/icon-earn.svg new file mode 100644 index 0000000000..62b3202efe --- /dev/null +++ b/frontend/src/assets/images/landing-page/icon-earn.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/assets/images/landing-page/icon-verify.svg b/frontend/src/assets/images/landing-page/icon-verify.svg new file mode 100644 index 0000000000..ddaa2c08da --- /dev/null +++ b/frontend/src/assets/images/landing-page/icon-verify.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/src/assets/images/landing-page/mina-intro-image.jpg b/frontend/src/assets/images/landing-page/mina-intro-image.jpg new file mode 100644 index 0000000000..ab0777f3fd Binary files /dev/null and b/frontend/src/assets/images/landing-page/mina-intro-image.jpg differ diff --git a/frontend/src/assets/images/landing-page/mina-intro-lettering.png b/frontend/src/assets/images/landing-page/mina-intro-lettering.png new file mode 100644 index 0000000000..4ebebde268 Binary files /dev/null and b/frontend/src/assets/images/landing-page/mina-intro-lettering.png differ diff --git a/frontend/src/assets/images/landing-page/tinified/blog-featured-image.jpg b/frontend/src/assets/images/landing-page/tinified/blog-featured-image.jpg new file mode 100644 index 0000000000..45f77587ca Binary files /dev/null and b/frontend/src/assets/images/landing-page/tinified/blog-featured-image.jpg differ diff --git a/frontend/src/assets/images/landing-page/tinified/cta-section-bg.png b/frontend/src/assets/images/landing-page/tinified/cta-section-bg.png new file mode 100644 index 0000000000..b33c9713ab Binary files /dev/null and b/frontend/src/assets/images/landing-page/tinified/cta-section-bg.png differ diff --git a/frontend/src/assets/images/landing-page/tinified/hero-image.jpg b/frontend/src/assets/images/landing-page/tinified/hero-image.jpg new file mode 100644 index 0000000000..7d6f2be276 Binary files /dev/null and b/frontend/src/assets/images/landing-page/tinified/hero-image.jpg differ diff --git a/frontend/src/assets/images/landing-page/tinified/mina-intro-image.jpg b/frontend/src/assets/images/landing-page/tinified/mina-intro-image.jpg new file mode 100644 index 0000000000..de8b494b73 Binary files /dev/null and b/frontend/src/assets/images/landing-page/tinified/mina-intro-image.jpg differ diff --git a/frontend/src/assets/images/landing-page/tinified/mina-intro-lettering.png b/frontend/src/assets/images/landing-page/tinified/mina-intro-lettering.png new file mode 100644 index 0000000000..5dd070b151 Binary files /dev/null and b/frontend/src/assets/images/landing-page/tinified/mina-intro-lettering.png differ diff --git a/frontend/src/assets/images/landing-page/tinified/web-node-bits-bg.png b/frontend/src/assets/images/landing-page/tinified/web-node-bits-bg.png new file mode 100644 index 0000000000..95b81b8804 Binary files /dev/null and b/frontend/src/assets/images/landing-page/tinified/web-node-bits-bg.png differ diff --git a/frontend/src/assets/images/landing-page/web-node-bits-bg.png b/frontend/src/assets/images/landing-page/web-node-bits-bg.png new file mode 100644 index 0000000000..4c7ad227ca Binary files /dev/null and b/frontend/src/assets/images/landing-page/web-node-bits-bg.png differ diff --git a/frontend/src/assets/images/logo/logo-text.svg b/frontend/src/assets/images/logo/logo-text.svg index de0c2f13cf..6b6386e607 100644 --- a/frontend/src/assets/images/logo/logo-text.svg +++ b/frontend/src/assets/images/logo/logo-text.svg @@ -1 +1,24 @@ - \ No newline at end of file + + + + + + + + diff --git a/frontend/src/assets/styles/leaderboard-variables.scss b/frontend/src/assets/styles/leaderboard-variables.scss new file mode 100644 index 0000000000..63c4f54c84 --- /dev/null +++ b/frontend/src/assets/styles/leaderboard-variables.scss @@ -0,0 +1,25 @@ +$mina-cta-primary: white; +$mina-base-divider: rgba(45, 45, 45, 0.10); +$white4: rgba(45, 45, 45, 0.4); +$mina-base-secondary: rgba(45, 45, 45, 0.60); +$mina-base-surface: rgba(226, 235, 239, 0.50); +$mina-base-container: #eff4f6; + +$black: rgba(0, 0, 0, 1); +$mina-base-primary: #2d2d2d; +$mina-cta-surface: #2d2d2d; +$black3: rgba(0, 0, 0, 0.20); +$black4: rgba(0, 0, 0, 0.40); +$black5: rgba(96, 96, 96, 1); +$black6: rgba(45, 45, 45, 1); + +$mina-access-primary: #8971fd; +$mina-brand-aqua: #bbfdf8; +$mina-brand-cyan: #31cdea; +$mina-brand-lilac: #e2dfff; +$mina-brand-peony: #f1dceb; +$mina-brand-gray: #d9d9d9; + +$mina-brand-gradient: linear-gradient(272deg, #b6eeff 2.39%, #f7f5ff 25.39%, #d7c4fa 48.39%, #f4c0da 71.4%, #ffc4a4 94.4%); +$mina-brand-gradient-reversed: linear-gradient(272deg, #ffc4a4 2.39%, #f4c0da 25.39%, #d7c4fa 48.39%, #f7f5ff 71.4%, #b6eeff 94.4%); +$mina-brand-gradient2: linear-gradient(45deg, #57d7ff 8%, #fda2ff 60%, #ff833d 100%); diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts index dc746a0189..27fb7cddd8 100644 --- a/frontend/src/environments/environment.ts +++ b/frontend/src/environments/environment.ts @@ -4,7 +4,7 @@ export const environment: Readonly = { production: false, identifier: 'Dev FE', canAddNodes: true, - showWebNodeLandingPage: false, + showWebNodeLandingPage: true, globalConfig: { features: { dashboard: [], @@ -18,6 +18,15 @@ export const environment: Readonly = { benchmarks: ['wallets'], fuzzing: [], }, + firebase: { + apiKey: 'AIzaSyBZzFsHjIbQVbBP0N-KkUsEvHRVU_wwd7g', + authDomain: 'webnode-gtm-test.firebaseapp.com', + projectId: 'webnode-gtm-test', + storageBucket: 'webnode-gtm-test.firebasestorage.app', + messagingSenderId: '1016673359357', + appId: '1:1016673359357:web:bbd2cbf3f031756aec7594', + measurementId: 'G-ENDBL923XT', + }, graphQL: 'https://adonagy.com/graphql', // graphQL: 'https://api.minascan.io/node/devnet/v1/graphql', // graphQL: 'http://65.109.105.40:5000/graphql', @@ -39,27 +48,27 @@ export const environment: Readonly = { // name: 'Producer-2', // url: 'https://staging-devnet-openmina-bp-2-dashboard.minaprotocol.network', // }, - { - name: 'staging-devnet-bp-0', - url: 'https://staging-devnet-openmina-bp-0.minaprotocol.network', - }, - { - name: 'staging-devnet-bp-1', - url: 'https://staging-devnet-openmina-bp-1.minaprotocol.network', - }, - { - name: 'staging-devnet-bp-2', - url: 'https://staging-devnet-openmina-bp-2.minaprotocol.network', - }, - { - name: 'staging-devnet-bp-3', - url: 'https://staging-devnet-openmina-bp-3.minaprotocol.network', - }, // { - // name: 'Web Node 1', - // isWebNode: true, + // name: 'staging-devnet-bp-0', + // url: 'https://staging-devnet-openmina-bp-0.minaprotocol.network', // }, // { + // name: 'staging-devnet-bp-1', + // url: 'https://staging-devnet-openmina-bp-1.minaprotocol.network', + // }, + // { + // name: 'staging-devnet-bp-2', + // url: 'https://staging-devnet-openmina-bp-2.minaprotocol.network', + // }, + // { + // name: 'staging-devnet-bp-3', + // url: 'https://staging-devnet-openmina-bp-3.minaprotocol.network', + // }, + { + name: 'Web Node 1', + isWebNode: true, + }, + // { // name: 'http://65.109.105.40:3000', // url: 'http://65.109.105.40:3000', // }, diff --git a/frontend/src/index.html b/frontend/src/index.html index 114811cd57..874924d364 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -49,13 +49,13 @@ + diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 1ab4659dc4..beff9d9483 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -1,6 +1,7 @@ @use '@angular/material' as mat; @import 'openmina'; +@import 'leaderboard-variables'; $custom-typography: mat.define-legacy-typography-config($font-family: '"Inter", sans-serif'); // TODO(v15): As of v15 mat.legacy-core no longer includes default typography styles. @@ -24,6 +25,8 @@ body { font-weight: 400; color: $base-primary; background-color: var(--base-background, #000000); + -webkit-tap-highlight-color: transparent; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } .theme-transition { diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 76e7db1fbb..ccc8161a14 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -80,6 +80,9 @@ ], "@fuzzing/*": [ "src/app/features/fuzzing/*" + ], + "@leaderboard/*": [ + "src/app/features/leaderboard/*" ] } },