Compare commits

...

1 Commits

Author SHA1 Message Date
Mononaut
283fd1c58e
add sp widget to custom dashboard 2024-12-30 17:22:25 +00:00
9 changed files with 347 additions and 1 deletions

View File

@ -38,7 +38,12 @@
}
},
{
"component": "blocks"
"component": "simpleproof",
"mobileOrder": 6,
"props": {
"label": "Executive Decrees",
"key": "el_salvador_decretos"
}
},
{
"component": "addressTransactions",

View File

@ -305,6 +305,20 @@
</div>
</div>
}
@case ('simpleproof') {
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card">
<div class="card-body">
<a class="title-link" href="" [routerLink]="['/sp/verified' | relativeUrl]">
<h5 class="card-title d-inline" i18n="dashboard.recent-blocks">{{ widget.props?.label }}</h5>
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
</a>
<app-simpleproof-widget [label]="widget.props.label" [key]="widget.props.key" [widget]="true"></app-simpleproof-widget>
</div>
</div>
</div>
}
}
}
</div>

View File

@ -0,0 +1,75 @@
<div class="container-xl" style="min-height: 335px" [ngClass]="{'widget': widget, 'full-height': !widget}">
<div *ngIf="!widget" class="float-left" style="display: flex; width: 100%; align-items: center;">
<h1>{{ label }}</h1>
<div *ngIf="!widget && isLoading" class="spinner-border" role="status"></div>
</div>
<div class="clearfix"></div>
@if (isLoading) {
loading!
<div class="spinner-wrapper">
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
} @else if (error || !verified.length) {
<div class="error-wrapper">
<span>temporarily unavailable</span>
</div>
} @else {
<div style="min-height: 295px">
<table class="table table-borderless" [class.table-fixed]="widget">
<thead>
<th class="filename text-left" [ngClass]="{'widget': widget}" i18n="simpleproof.filename">Filename</th>
<th class="hash text-left" [ngClass]="{'widget': widget}" i18n="simpleproof.hash">Hash</th>
<th class="verified text-right" [ngClass]="{'widget': widget}" i18n="simpleproof.verified">Verified</th>
<th class="proof text-right" [ngClass]="{'widget': widget}" i18n="simpleproof.proof">Proof</th>
</thead>
<tbody *ngIf="verifiedPage; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<tr *ngFor="let item of verifiedPage">
<td class="filename text-left" [class]="widget ? 'widget' : ''">{{ item.file_name }}</td>
<td class="hash text-left" [class]="widget ? 'widget' : ''">{{ item.sha256 }}</td>
<td class="verified text-right" [class]="widget ? 'widget' : ''">
<app-timestamp [unixTime]="item.block_time" [customFormat]="'yyyy-MM-dd'" [hideTimeSince]="true"></app-timestamp>
</td>
<td class="proof text-right" [class]="widget ? 'widget' : ''">
<a [href]="item.sanitized_url" target="_blank" class="badge badge-primary badge-verify">
<span class="icon">
<img class="icon-img" src="/resources/sp.svg">
</span>
<span i18n="simpleproof.verify">Verify</span>
</a>
</td>
</tr>
</tbody>
<ng-template #skeleton>
<tbody>
<tr *ngFor="let item of [].constructor(itemsPerPage)">
<td class="filename text-left" [ngClass]="{'widget': widget}">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="hash text-left" [ngClass]="{'widget': widget}">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="verified text-right" [ngClass]="{'widget': widget}">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="proof text-right" [ngClass]="{'widget': widget}">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
</tr>
</tbody>
</ng-template>
</table>
<ngb-pagination *ngIf="!widget" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
[collectionSize]="verified.length" [rotate]="true" [maxSize]="paginationMaxSize" [pageSize]="itemsPerPage" [(page)]="page"
(pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
</ngb-pagination>
<ng-template [ngIf]="!widget">
<div class="clearfix"></div>
<br>
</ng-template>
</div>
}
</div>

View File

@ -0,0 +1,114 @@
.spinner-wrapper, .error-wrapper {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
}
.spinner-border {
height: 25px;
width: 25px;
margin-top: -10px;
margin-left: -13px;
flex-shrink: 0;
}
.container-xl {
max-width: 1400px;
}
.container-xl.widget {
padding-left: 0px;
padding-bottom: 0px;
padding-right: 0px;
}
.container-xl.legacy {
max-width: 1140px;
}
.container {
max-width: 100%;
}
tr, td, th {
border: 0px;
padding-top: 0.71rem !important;
padding-bottom: 0.7rem !important;
}
.clear-link {
color: white;
}
.disabled {
pointer-events: none;
opacity: 0.5;
}
.progress {
background-color: var(--secondary);
}
.filename {
width: 50%;
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hash {
width: 25%;
max-width: 700px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
td.hash {
font-family: monospace;
}
.widget .hash {
display: none;
}
@media (max-width: 1200px) {
.hash {
display: none;
}
}
.verified {
width: 25%;
}
td.verified {
color: var(--tertiary);
}
.proof {
width: 25%;
}
.badge-verify {
font-size: 1.05em;
font-weight: normal;
background: var(--nav-bg);
color: white;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: auto;
.icon {
margin: -0.25em;
margin-right: 0.5em;
.icon-img {
width: 16px;
font-size: 16px;
}
}
}

View File

@ -0,0 +1,86 @@
import { Component, Input, SecurityContext, SimpleChanges, OnChanges } from '@angular/core';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { ServicesApiServices } from '@app/services/services-api.service';
import { catchError, of } from 'rxjs';
export interface SimpleProof {
file_name: string;
sha256: string;
ots_verification: string;
block_height: number;
block_hash: string;
block_time: number;
simpleproof_url: string;
key?: string;
sanitized_url?: SafeResourceUrl;
}
@Component({
selector: 'app-simpleproof-widget',
templateUrl: './simpleproof-widget.component.html',
styleUrls: ['./simpleproof-widget.component.scss'],
})
export class SimpleProofWidgetComponent implements OnChanges {
@Input() key: string = window['__env']?.customize?.dashboard.widgets?.find(w => w.component ==='simpleproof')?.props?.key ?? '';
@Input() label: string = window['__env']?.customize?.dashboard.widgets?.find(w => w.component ==='simpleproof')?.props?.label ?? 'Verified Documents';
@Input() widget: boolean = false;
@Input() width = 300;
@Input() height = 400;
verified: SimpleProof[] = [];
verifiedPage: SimpleProof[] = [];
isLoading: boolean = true;
error: boolean = false;
page = 1;
lastPage = 1;
itemsPerPage = 15;
paginationMaxSize = window.innerWidth <= 767.98 ? 3 : 5;
constructor(
private servicesApiService: ServicesApiServices,
public sanitizer: DomSanitizer,
) {}
ngOnInit(): void {
this.loadVerifications();
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.widget) {
this.itemsPerPage = this.widget ? 6 : 15;
}
if (changes.key) {
this.loadVerifications();
}
}
loadVerifications(): void {
if (this.key) {
this.isLoading = true;
this.servicesApiService.getSimpleProofs$(this.key).pipe(
catchError(() => {
this.isLoading = false;
this.error = true;
return of({});
}),
).subscribe((data: Record<string, SimpleProof>) => {
if (Object.keys(data).length) {
this.verified = Object.keys(data).map(key => ({
...data[key],
file_name: data[key].file_name.replace('source-', '').replace('_', ' '),
key,
sanitized_url: this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL, data[key]['simpleproof-url']) ?? ''),
})).sort((a, b) => b.key.localeCompare(a.key));
this.verifiedPage = this.verified.slice((this.page - 1) * this.itemsPerPage, this.page * this.itemsPerPage);
this.isLoading = false;
this.error = false;
}
});
}
}
pageChange(page: number): void {
this.page = page;
this.verifiedPage = this.verified.slice((this.page - 1) * this.itemsPerPage, this.page * this.itemsPerPage);
}
}

View File

@ -13,6 +13,7 @@ import { RbfList } from '@components/rbf-list/rbf-list.component';
import { ServerHealthComponent } from '@components/server-health/server-health.component';
import { ServerStatusComponent } from '@components/server-health/server-status.component';
import { FaucetComponent } from '@components/faucet/faucet.component'
import { SimpleProofWidgetComponent } from './components/simpleproof-widget/simpleproof-widget.component';
const browserWindow = window || {};
// @ts-ignore
@ -130,6 +131,13 @@ if (window['__env']?.OFFICIAL_MEMPOOL_SPACE) {
}
}
if (window['__env']?.customize?.dashboard.widgets?.some(w => w.component ==='simpleproof')) {
routes[0].children.push({
path: 'sp/verified',
component: SimpleProofWidgetComponent,
});
}
@NgModule({
imports: [
RouterModule.forChild(routes)

View File

@ -8,6 +8,7 @@ import { Observable, of, ReplaySubject, tap, catchError, share, filter, switchMa
import { IBackendInfo } from '@interfaces/websocket.interface';
import { Acceleration, AccelerationHistoryParams } from '@interfaces/node-api.interface';
import { AccelerationStats } from '@components/acceleration/acceleration-stats/acceleration-stats.component';
import { SimpleProof } from '../components/simpleproof-widget/simpleproof-widget.component';
export interface IUser {
username: string;
@ -217,4 +218,8 @@ export class ServicesApiServices {
getPaymentStatus$(orderId: string): Observable<any> {
return this.httpClient.get<any>(`${this.stateService.env.SERVICES_API}/payments/bitcoin/check?order_id=${orderId}`, { observe: 'response' });
}
getSimpleProofs$(key: string): Observable<Record<string, SimpleProof>> {
return this.httpClient.get<Record<string, SimpleProof>>(`${this.stateService.env.SERVICES_API}/sp/verified/${key}`);
}
}

View File

@ -116,6 +116,7 @@ import { CalculatorComponent } from '@components/calculator/calculator.component
import { BitcoinsatoshisPipe } from '@app/shared/pipes/bitcoinsatoshis.pipe';
import { HttpErrorComponent } from '@app/shared/components/http-error/http-error.component';
import { TwitterWidgetComponent } from '@components/twitter-widget/twitter-widget.component';
import { SimpleProofWidgetComponent } from '@components/simpleproof-widget/simpleproof-widget.component';
import { FaucetComponent } from '@components/faucet/faucet.component';
import { TwitterLogin } from '@components/twitter-login/twitter-login.component';
import { BitcoinInvoiceComponent } from '@components/bitcoin-invoice/bitcoin-invoice.component';
@ -235,6 +236,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/
OrdDataComponent,
HttpErrorComponent,
TwitterWidgetComponent,
SimpleProofWidgetComponent,
FaucetComponent,
TwitterLogin,
BitcoinInvoiceComponent,
@ -369,6 +371,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/
OrdDataComponent,
HttpErrorComponent,
TwitterWidgetComponent,
SimpleProofWidgetComponent,
TwitterLogin,
BitcoinInvoiceComponent,
BitcoinsatoshisPipe,

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="Layer_1"
version="1.1"
viewBox="0 0 492.10001 575.79999"
width="492.10001"
height="575.79999"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<!-- Generator: Adobe Illustrator 29.0.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 186) -->
<defs
id="defs184">
<style
id="style182">
.st0 {
fill: #fff;
}
.st1 {
fill: #f88e2b;
}
</style>
</defs>
<g
id="g216"
transform="translate(-159.5,-152.1)">
<polygon
class="st0"
points="296.6,375.6 296.6,459.1 404.9,524.2 651.6,378.5 651.6,294.6 651.6,294.5 405.3,440.4 "
id="polygon212" />
<polygon
class="st1"
points="405.5,644.5 231.3,542 231.3,335.6 405.5,235 520.9,301.7 592.1,259.8 405.5,152.1 159.5,294.1 159.5,583.1 405.5,727.9 651.6,583.1 651.6,447.1 579.7,489.4 579.7,542 "
id="polygon214" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 996 B