Merge pull request #5935 from mempool/mononaut/sp-cubo-widget

sp cubo widget
This commit is contained in:
wiz
2025-06-26 09:24:55 +09:00
committed by GitHub
12 changed files with 643 additions and 2 deletions

View File

@@ -39,7 +39,12 @@
}
},
{
"component": "blocks"
"component": "simpleproof_cubo",
"mobileOrder": 6,
"props": {
"label": "CUBO+ Certificates",
"key": "cubo_certificates"
}
},
{
"component": "walletTransactions",

View File

@@ -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>&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>
}
@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>&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-cubo-widget [label]="widget.props.label" [key]="widget.props.key" [widget]="true"></app-simpleproof-cubo-widget>
</div>
</div>
</div>
}
}
}
</div>

View File

@@ -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>

View File

@@ -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();
}
}

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,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;
}
}
}

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

@@ -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)

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;
@@ -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}`);
}
}

View File

@@ -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);
}
}

View 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

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