diff --git a/frontend/src/app/lightning/group/group-preview.component.html b/frontend/src/app/lightning/group/group-preview.component.html new file mode 100644 index 000000000..8a918be35 --- /dev/null +++ b/frontend/src/app/lightning/group/group-preview.component.html @@ -0,0 +1,56 @@ +
+ + Lightning node group + +
+
+ +
+
+

{{ group.name }}

+
+
+
+
+
{{ group.description }}
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + +
Nodes{{ nodes.nodes.length }}
Liquidity + + + {{ nodes.sumLiquidity | amountShortener: 1 }} + sats + +   +
Channels{{ nodes.sumChannels }}
Average size + +
+
+
+ +
+
+
\ No newline at end of file diff --git a/frontend/src/app/lightning/group/group-preview.component.scss b/frontend/src/app/lightning/group/group-preview.component.scss new file mode 100644 index 000000000..712112f5a --- /dev/null +++ b/frontend/src/app/lightning/group/group-preview.component.scss @@ -0,0 +1,65 @@ +.table { + font-size: 32px; + margin-top: 0px; +} + +.logo-wrapper { + position: relative; + width: 62px; + height: 62px; + margin-right: 1em; + + img { + position: absolute; + right: 0; + top: 0; + } +} + +.description-wrapper { + width: 100%; + margin: 16px 0 0; + padding: 20px 12px; + background: #181b2d; + font-size: 32px; +} + +.description-text { + width: 100%; + line-height: 36px; + height: 72px; + max-height: 72px; + min-height: 72px; + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + text-overflow: ellipsis; +} + +.map-col { + flex-grow: 0; + flex-shrink: 0; + width: 470px; + height: 272px; + min-width: 470px; + min-height: 272px; + max-height: 272px; + padding: 0; + background: #181b2d; + overflow: hidden; + margin-top: 16px; +} + +.row { + margin-right: 0; +} + +.full-width-row { + padding-left: 15px; + flex-wrap: nowrap; +} + +::ng-deep .symbol { + font-size: 24px; +} \ No newline at end of file diff --git a/frontend/src/app/lightning/group/group-preview.component.ts b/frontend/src/app/lightning/group/group-preview.component.ts new file mode 100644 index 000000000..be23e6178 --- /dev/null +++ b/frontend/src/app/lightning/group/group-preview.component.ts @@ -0,0 +1,124 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, ParamMap } from '@angular/router'; +import { map, switchMap, Observable, catchError, of } from 'rxjs'; +import { SeoService } from '../../services/seo.service'; +import { OpenGraphService } from '../../services/opengraph.service'; +import { GeolocationData } from '../../shared/components/geolocation/geolocation.component'; +import { LightningApiService } from '../lightning-api.service'; + +interface NodeGroup { + name: string; + description: string; +} + +@Component({ + selector: 'app-group-preview', + templateUrl: './group-preview.component.html', + styleUrls: ['./group-preview.component.scss'] +}) +export class GroupPreviewComponent implements OnInit { + nodes$: Observable; + group: NodeGroup = { name: '', description: '' }; + slug: string; + groupId: string; + + constructor( + private lightningApiService: LightningApiService, + private activatedRoute: ActivatedRoute, + private seoService: SeoService, + private openGraphService: OpenGraphService, + ) { } + + ngOnInit(): void { + this.seoService.setTitle(`Mempool.Space Lightning Nodes`); + + this.nodes$ = this.activatedRoute.paramMap + .pipe( + switchMap((params: ParamMap) => { + this.slug = params.get('slug'); + this.openGraphService.waitFor('ln-group-map-' + this.slug); + this.openGraphService.waitFor('ln-group-data-' + this.slug); + + if (this.slug === 'the-mempool-open-source-project') { + this.groupId = 'mempool.space'; + this.group = { + name: 'The Mempool Open Source Project', + description: 'These are the Lightning nodes operated by The Mempool Open Source Project that provide data for the mempool.space website. Connect to us!', + }; + } else { + this.group = { + name: this.slug.replace(/-/gi, ' '), + description: '', + }; + this.openGraphService.fail('ln-group-map-' + this.slug); + this.openGraphService.fail('ln-group-data-' + this.slug); + return of(null); + } + + return this.lightningApiService.getNodGroupNodes$(this.groupId); + }), + map((nodes) => { + for (const node of nodes) { + const socketsObject = []; + for (const socket of node.sockets.split(',')) { + if (socket === '') { + continue; + } + let label = ''; + if (socket.match(/(?:[0-9]{1,3}\.){3}[0-9]{1,3}/)) { + label = 'IPv4'; + } else if (socket.indexOf('[') > -1) { + label = 'IPv6'; + } else if (socket.indexOf('onion') > -1) { + label = 'Tor'; + } + socketsObject.push({ + label: label, + socket: node.public_key + '@' + socket, + }); + } + // @ts-ignore + node.socketsObject = socketsObject; + + if (!node?.country && !node?.city && + !node?.subdivision) { + // @ts-ignore + node.geolocation = null; + } else { + // @ts-ignore + node.geolocation = { + country: node.country?.en, + city: node.city?.en, + subdivision: node.subdivision?.en, + iso: node.iso_code, + }; + } + } + const sumLiquidity = nodes.reduce((partialSum, a) => partialSum + parseInt(a.capacity, 10), 0); + const sumChannels = nodes.reduce((partialSum, a) => partialSum + a.opened_channel_count, 0); + + this.openGraphService.waitOver('ln-group-data-' + this.slug); + + return { + nodes: nodes, + sumLiquidity: sumLiquidity, + sumChannels: sumChannels, + }; + }), + catchError(() => { + this.openGraphService.fail('ln-group-map-' + this.slug); + this.openGraphService.fail('ln-group-data-' + this.slug); + return of({ + nodes: [], + sumLiquidity: 0, + sumChannels: 0, + }); + }) + ); + } + + onMapReady(): void { + this.openGraphService.waitOver('ln-group-map-' + this.slug); + } + +} diff --git a/frontend/src/app/lightning/lightning-previews.module.ts b/frontend/src/app/lightning/lightning-previews.module.ts index 0400acc55..c41ba8d20 100644 --- a/frontend/src/app/lightning/lightning-previews.module.ts +++ b/frontend/src/app/lightning/lightning-previews.module.ts @@ -9,11 +9,13 @@ import { NodePreviewComponent } from './node/node-preview.component'; import { LightningPreviewsRoutingModule } from './lightning-previews.routing.module'; import { ChannelPreviewComponent } from './channel/channel-preview.component'; import { NodesPerISPPreview } from './nodes-per-isp/nodes-per-isp-preview.component'; +import { GroupPreviewComponent } from './group/group-preview.component'; @NgModule({ declarations: [ NodePreviewComponent, ChannelPreviewComponent, NodesPerISPPreview, + GroupPreviewComponent, ], imports: [ CommonModule, diff --git a/frontend/src/app/lightning/lightning-previews.routing.module.ts b/frontend/src/app/lightning/lightning-previews.routing.module.ts index 11250214d..6cce90766 100644 --- a/frontend/src/app/lightning/lightning-previews.routing.module.ts +++ b/frontend/src/app/lightning/lightning-previews.routing.module.ts @@ -3,6 +3,7 @@ import { RouterModule, Routes } from '@angular/router'; import { NodePreviewComponent } from './node/node-preview.component'; import { ChannelPreviewComponent } from './channel/channel-preview.component'; import { NodesPerISPPreview } from './nodes-per-isp/nodes-per-isp-preview.component'; +import { GroupPreviewComponent } from './group/group-preview.component'; const routes: Routes = [ { @@ -17,6 +18,10 @@ const routes: Routes = [ path: 'nodes/isp/:isp', component: NodesPerISPPreview, }, + { + path: 'group/:slug', + component: GroupPreviewComponent, + }, { path: '**', redirectTo: '' diff --git a/unfurler/src/routes.ts b/unfurler/src/routes.ts index 3dfa66b5f..f9280369c 100644 --- a/unfurler/src/routes.ts +++ b/unfurler/src/routes.ts @@ -56,6 +56,13 @@ const routes = { } } } + }, + group: { + render: true, + params: 1, + getTitle(path) { + return `Lightning Node Group: ${path[0]}`; + } } } },