mirror of
https://github.com/mempool/mempool.git
synced 2025-09-20 22:36:19 +02:00
Merge pull request #5935 from mempool/mononaut/sp-cubo-widget
sp cubo widget
This commit is contained in:
@@ -39,7 +39,12 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "blocks"
|
||||
"component": "simpleproof_cubo",
|
||||
"mobileOrder": 6,
|
||||
"props": {
|
||||
"label": "CUBO+ Certificates",
|
||||
"key": "cubo_certificates"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "walletTransactions",
|
||||
|
@@ -307,6 +307,37 @@
|
||||
</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">{{ widget.props?.label }}</h5>
|
||||
<span> </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>
|
||||
}
|
||||
@case ('simpleproof_cubo') {
|
||||
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<a href="" [routerLink]="['/sp/cubo' | relativeUrl]">
|
||||
<h5 class="card-title d-inline">
|
||||
<img src="/resources/cubo.svg" style="width: 1.1em; height: 1.1em; margin-right: 0.1em; transform: translateY(-0.1em);">
|
||||
{{ widget.props?.label }}
|
||||
</h5>
|
||||
<span> </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-cubo-widget [label]="widget.props.label" [key]="widget.props.key" [widget]="true"></app-simpleproof-cubo-widget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
@@ -0,0 +1,104 @@
|
||||
<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>
|
||||
<img src="/resources/cubo.svg" style="width: 1.1em; height: 1.1em; margin-right: 0.1em; transform: translateY(-0.1em);">
|
||||
{{ 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">
|
||||
<div *ngIf="!widget" class="search-container mb-3">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
(input)="applyFilter($event)"
|
||||
placeholder="Search by student name or ID..."
|
||||
i18n-placeholder="simpleproof.search_placeholder"
|
||||
>
|
||||
</div>
|
||||
<table class="table table-borderless" [class.table-fixed]="widget">
|
||||
<thead>
|
||||
<th class="filename text-left" [ngClass]="{'widget': widget}" i18n="simpleproof.student_name">Student Name</th>
|
||||
<th class="id text-left" [ngClass]="{'widget': widget}" i18n="simpleproof.id_code">ID</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 *ngIf="widget" class="search-row">
|
||||
<td colspan="3" class="p-0" style="padding: 0 0 6px !important;">
|
||||
<div class="search-container">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
(input)="applyFilter($event)"
|
||||
placeholder="Search by student name or ID..."
|
||||
i18n-placeholder="simpleproof.search_placeholder"
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngFor="let item of verifiedPage">
|
||||
<td class="filename text-left" [class]="widget ? 'widget' : ''">{{ item.student_name }}</td>
|
||||
<td class="id text-left" [class]="widget ? 'widget' : ''">
|
||||
@if (item.sanitized_download_url) {
|
||||
<a [href]="item.sanitized_download_url" target="_blank">
|
||||
<span>{{ item.id_code }}</span>
|
||||
<span class="icon ml-2">
|
||||
<fa-icon [icon]="['fas', 'file-pdf']" [fixedWidth]="true"></fa-icon>
|
||||
</span>
|
||||
</a>
|
||||
} @else {
|
||||
{{ item.id_code }}
|
||||
}
|
||||
</td>
|
||||
<td class="proof text-right" [class]="widget ? 'widget' : ''">
|
||||
<a [href]="item.sanitized_simpleproof_url" target="_blank" class="badge badge-primary badge-verify" style="font-size: 1.035em;">
|
||||
<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="id text-left" [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>
|
@@ -0,0 +1,137 @@
|
||||
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 SimpleProofCubo {
|
||||
student_name: string;
|
||||
id_code: string;
|
||||
download_url: string;
|
||||
simpleproof_url: string;
|
||||
sanitized_download_url: SafeResourceUrl;
|
||||
sanitized_simpleproof_url: SafeResourceUrl;
|
||||
parsed?: { type: string; year: number; studentNumber: number };
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-simpleproof-cubo-widget',
|
||||
templateUrl: './simpleproof-cubo-widget.component.html',
|
||||
styleUrls: ['./simpleproof-widget.component.scss'],
|
||||
})
|
||||
export class SimpleProofCuboWidgetComponent implements OnChanges {
|
||||
@Input() key: string = window['__env']?.customize?.dashboard.widgets?.find(w => w.component ==='simpleproof_cubo')?.props?.key ?? '';
|
||||
@Input() label: string = window['__env']?.customize?.dashboard.widgets?.find(w => w.component ==='simpleproof_cubo')?.props?.label ?? 'CUBO+ Certificates';
|
||||
@Input() widget: boolean = false;
|
||||
@Input() width = 300;
|
||||
@Input() height = 400;
|
||||
|
||||
searchText: string = '';
|
||||
verified: SimpleProofCubo[] = [];
|
||||
filteredVerified: SimpleProofCubo[] = [];
|
||||
verifiedPage: SimpleProofCubo[] = [];
|
||||
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 ? 5 : 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, SimpleProofCubo>) => {
|
||||
if (Object.keys(data).length) {
|
||||
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
|
||||
this.verified = Object.keys(data).map(key => ({
|
||||
...data[key],
|
||||
key,
|
||||
parsed: this.parseCuboKey(key),
|
||||
sanitized_download_url: data[key]['download_url']?.length ? this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL, data[key]['download_url']) ?? '') : null,
|
||||
sanitized_simpleproof_url: data[key]['simpleproof_url']?.length ? this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL, data[key]['simpleproof_url']) ?? '') : null,
|
||||
})).sort((a, b) => {
|
||||
// smarter sorting using the specific Cubo ID format, where possible
|
||||
if (a.parsed && b.parsed) {
|
||||
if (a.parsed.year !== b.parsed.year) {
|
||||
return b.parsed.year - a.parsed.year;
|
||||
}
|
||||
if (a.parsed.type !== b.parsed.type) {
|
||||
return a.parsed.type.localeCompare(b.parsed.type);
|
||||
}
|
||||
return a.parsed.studentNumber - b.parsed.studentNumber;
|
||||
}
|
||||
// fallback to lexicographic sorting
|
||||
if (!a.parsed && !b.parsed) {
|
||||
return collator.compare(b.key, a.key);
|
||||
}
|
||||
return a.parsed ? -1 : 1;
|
||||
});
|
||||
this.applyFilter();
|
||||
this.isLoading = false;
|
||||
this.error = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
parseCuboKey(key: string): { type: string; year: number; studentNumber: number } | null {
|
||||
const match = key.match(/^Cubo\+([A-Za-z]*)(\d{4})-(\d+)$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const [, type, yearStr, studentNumberStr] = match;
|
||||
return {
|
||||
type: type || '',
|
||||
year: parseInt(yearStr, 10),
|
||||
studentNumber: parseInt(studentNumberStr, 10)
|
||||
};
|
||||
}
|
||||
|
||||
applyFilter(event?: Event): void {
|
||||
let searchText = '';
|
||||
if (event) {
|
||||
searchText = (event.target as HTMLInputElement).value;
|
||||
}
|
||||
if (searchText?.length > 0) {
|
||||
this.filteredVerified = this.verified.filter(item =>
|
||||
item.student_name.toLowerCase().includes(searchText.toLowerCase()) || item.id_code.toLowerCase().includes(searchText.toLowerCase())
|
||||
);
|
||||
} else {
|
||||
this.filteredVerified = this.verified;
|
||||
}
|
||||
this.page = 1;
|
||||
this.updatePage();
|
||||
}
|
||||
|
||||
updatePage(): void {
|
||||
this.verifiedPage = this.filteredVerified.slice((this.page - 1) * this.itemsPerPage, this.page * this.itemsPerPage);
|
||||
}
|
||||
|
||||
pageChange(page: number): void {
|
||||
this.page = page;
|
||||
this.updatePage();
|
||||
}
|
||||
}
|
@@ -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>
|
@@ -0,0 +1,118 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.id {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -14,6 +14,8 @@ import { StratumList } from '@components/stratum/stratum-list/stratum-list.compo
|
||||
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';
|
||||
import { SimpleProofCuboWidgetComponent } from './components/simpleproof-widget/simpleproof-cubo-widget.component';
|
||||
|
||||
const browserWindow = window || {};
|
||||
// @ts-ignore
|
||||
@@ -141,6 +143,20 @@ 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,
|
||||
});
|
||||
}
|
||||
|
||||
if (window['__env']?.customize?.dashboard.widgets?.some(w => w.component ==='simpleproof_cubo')) {
|
||||
routes[0].children.push({
|
||||
path: 'sp/cubo',
|
||||
component: SimpleProofCuboWidgetComponent,
|
||||
});
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes)
|
||||
|
@@ -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;
|
||||
@@ -221,4 +222,10 @@ 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>> {
|
||||
// Need to use relative path here to avoid CORS errors, since this won't be used from mempool.space website
|
||||
const pathname = new URL(this.stateService.env.SERVICES_API + '/sp/verified').pathname;
|
||||
return this.httpClient.get<Record<string, SimpleProof>>(`${pathname}/${key}`);
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
|
||||
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faCogs, faDatabase, faExchangeAlt, faInfoCircle,
|
||||
@@ -8,7 +9,7 @@ import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, fa
|
||||
faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck,
|
||||
faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faTimeline,
|
||||
faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot, faShareNodes, faCreditCard, faMicroscope, faExclamationTriangle, faLockOpen, faPaperclip, faAddressCard,
|
||||
faMedal, faBug } from '@fortawesome/free-solid-svg-icons';
|
||||
faMedal, faBug, faFilePdf } from '@fortawesome/free-solid-svg-icons';
|
||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||
import { MenuComponent } from '@components/menu/menu.component';
|
||||
import { PreviewTitleComponent } from '@components/master-page-preview/preview-title.component';
|
||||
@@ -123,6 +124,8 @@ 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 { SimpleProofCuboWidgetComponent } from '@components/simpleproof-widget/simpleproof-cubo-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';
|
||||
@@ -246,6 +249,8 @@ import { GithubLogin } from '../components/github-login.component/github-login.c
|
||||
OrdDataComponent,
|
||||
HttpErrorComponent,
|
||||
TwitterWidgetComponent,
|
||||
SimpleProofWidgetComponent,
|
||||
SimpleProofCuboWidgetComponent,
|
||||
FaucetComponent,
|
||||
TwitterLogin,
|
||||
GithubLogin,
|
||||
@@ -383,6 +388,8 @@ import { GithubLogin } from '../components/github-login.component/github-login.c
|
||||
OrdDataComponent,
|
||||
HttpErrorComponent,
|
||||
TwitterWidgetComponent,
|
||||
SimpleProofWidgetComponent,
|
||||
SimpleProofCuboWidgetComponent,
|
||||
TwitterLogin,
|
||||
GithubLogin,
|
||||
BitcoinInvoiceComponent,
|
||||
@@ -470,5 +477,6 @@ export class SharedModule {
|
||||
library.addIcons(faMedal);
|
||||
library.addIcons(faAddressCard);
|
||||
library.addIcons(faBug);
|
||||
library.addIcons(faFilePdf);
|
||||
}
|
||||
}
|
||||
|
18
frontend/src/resources/cubo.svg
Normal file
18
frontend/src/resources/cubo.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Capa_2" data-name="Capa 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 273.87 291.85">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #235fa9;
|
||||
stroke-width: 0px;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Capa_1-2" data-name="Capa 1">
|
||||
<g id="b">
|
||||
<g id="c">
|
||||
<path class="cls-1" d="M246.41,177.71c-14.12,0-25.74,10.64-27.32,24.33h0c-30.93,17.72-61.86,35.47-92.79,53.21l-95.63-55.78v-38.36l95.87,55.54,125-73.72v-69.98L125.28,0,5.26,70.48l89.4,51.3,33.36-19.92c5.86,3.74,11.73,7.45,17.57,11.19.91,13.64,12.23,24.44,26.11,24.44s26.19-11.73,26.19-26.19-11.73-26.19-26.19-26.19c-3.41,0-6.67.67-9.67,1.87h0c-11.69-6.91-23.35-13.79-35.03-20.7l-32.86,18.68-25.89-15.18,57.51-33.36,95.87,55.28v34.12l-95.87,55.04L0,108.62v110.08l125.5,73.15c35.71-20.92,71.44-41.83,107.14-62.75v-.04c4.02,2.35,8.71,3.69,13.71,3.69,15.2,0,27.52-12.32,27.52-27.52s-12.32-27.52-27.52-27.52h.07ZM171.37,102.87c4.95,0,8.97,4.02,8.97,8.97s-4.02,8.97-8.97,8.97-8.97-4.02-8.97-8.97,4.02-8.97,8.97-8.97ZM246.28,214.51c-4.95,0-8.97-4.02-8.97-8.97s4.02-8.97,8.97-8.97,8.97,4.02,8.97,8.97-4.02,8.97-8.97,8.97Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
36
frontend/src/resources/sp.svg
Normal file
36
frontend/src/resources/sp.svg
Normal 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 |
Reference in New Issue
Block a user