Featured assets and asset groups

This commit is contained in:
softsimon
2022-02-06 01:20:26 +04:00
parent 755c1da8b3
commit 2e5c8bdfd3
19 changed files with 421 additions and 110 deletions

View File

@@ -0,0 +1,26 @@
<div *ngIf="group$ | async as group">
<div class="main-title">
<h2>{{ group.group.name }}</h2>
<div class="sub-title">Group of {{ group.group.assets.length | number }} assets</div>
</div>
<div class="clearfix"></div>
<br>
<div class="featuredBox">
<div *ngFor="let asset of group.assets">
<div class="card">
<img class="assetIcon" [src]="'https://liquid.network/api/v1/asset/' + asset.asset_id + '/icon'">
<div class="title">
<a [routerLink]="['/assets/asset/', group.asset_id]">{{ asset.name }}</a>
</div>
<div class="ticker">{{ asset.ticker }}</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,57 @@
.image {
width: 150px;
float: left;
}
.main-title {
float: left
}
.sub-title {
color: grey;
}
.featuredBox {
display: flex;
flex-flow: row wrap;
justify-content: center;
gap: 27px;
}
.card {
background-color: #1d1f31;
width: 200px;
height: 200px;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
.title {
font-size: 14px;
font-weight: bold;
margin-top: 10px;
}
.sub-title {
color: grey;
}
.assetIcon {
width: 100px;
height: 100px;
}
.image {
width: 100px;
height: 100px;
align-self: center;
}
.view-link {
margin-top: 30px;
}
.ticker {
color: grey;
}

View File

@@ -0,0 +1,45 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { combineLatest, Observable } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { ApiService } from 'src/app/services/api.service';
import { AssetsService } from 'src/app/services/assets.service';
@Component({
selector: 'app-asset-group',
templateUrl: './asset-group.component.html',
styleUrls: ['./asset-group.component.scss']
})
export class AssetGroupComponent implements OnInit {
group$: Observable<any>;
constructor(
private route: ActivatedRoute,
private apiService: ApiService,
private assetsService: AssetsService,
) { }
ngOnInit(): void {
this.group$ = this.route.paramMap
.pipe(
switchMap((params: ParamMap) => {
return combineLatest([
this.assetsService.getAssetsJson$,
this.apiService.getAssetGroup$(params.get('id')),
]);
}),
map(([assets, group]) => {
const items = [];
// @ts-ignore
for (const item of group.assets) {
items.push(assets[item]);
}
console.log(group);
return {
group: group,
assets: items
};
})
);
}
}

View File

@@ -0,0 +1,27 @@
<div *ngIf="featuredAssets$ | async as featured; else loading" class="featuredBox">
<div *ngFor="let group of featured.featured">
<div class="card">
<ng-template [ngIf]="group.assets" [ngIfElse]="singleAsset">
<img class="assetIcon" [src]="'https://liquid.network/api/v1/asset/' + group.assets[0] + '/icon'">
<div class="title"><a [routerLink]="['/assets/asset-group', group.id]">{{ group.name }}</a></div>
<div class="sub-title">Group of {{ group.assets.length | number }} assets</div>
</ng-template>
<ng-template #singleAsset>
<img class="assetIcon" [src]="'https://liquid.network/api/v1/asset/' + group.asset + '/icon'">
<div class="title">
<a [routerLink]="['/assets/asset/', group.asset]">{{ group.name }}</a>
</div>
<div class="ticker">{{ group.ticker }}</div>
</ng-template>
</div>
</div>
</div>
<ng-template #loading>
<br>
<div class="text-center loadingGraphs">
<div class="spinner-border text-light"></div>
</div>
</ng-template>

View File

@@ -0,0 +1,46 @@
.featuredBox {
display: flex;
flex-flow: row wrap;
justify-content: center;
gap: 27px;
}
.card {
background-color: #1d1f31;
width: 200px;
height: 200px;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
.title {
font-size: 14px;
font-weight: bold;
margin-top: 10px;
}
.sub-title {
color: grey;
font-size: 12px;
}
.assetIcon {
width: 100px;
height: 100px;
}
.image {
width: 100px;
height: 100px;
align-self: center;
}
.view-link {
margin-top: 30px;
}
.ticker {
color: grey;
}

View File

@@ -0,0 +1,34 @@
import { Component, OnInit } from '@angular/core';
import { combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ApiService } from 'src/app/services/api.service';
import { AssetsService } from 'src/app/services/assets.service';
@Component({
selector: 'app-assets-featured',
templateUrl: './assets-featured.component.html',
styleUrls: ['./assets-featured.component.scss']
})
export class AssetsFeaturedComponent implements OnInit {
featuredAssets$: Observable<any>;
constructor(
private apiService: ApiService,
private assetsService: AssetsService,
) { }
ngOnInit(): void {
this.featuredAssets$ = combineLatest([
this.assetsService.getAssetsJson$,
this.apiService.listFeaturedAssets$(),
]).pipe(
map(([assetsJson, featured]) => {
return {
assetsJson: assetsJson,
featured: featured,
};
})
);
}
}

View File

@@ -0,0 +1,31 @@
<div class="container-xl">
<div class="title-asset">
<h1 i18n="Assets page header">Assets</h1>
</div>
<ul class="nav nav-pills">
<li class="nav-item">
<a class="nav-link" [routerLink]="['/assets/featured']" routerLinkActive="active">Featured</a>
</li>
<li class="nav-item">
<a class="nav-link" [routerLink]="['/assets/all']" routerLinkActive="active">All</a>
</li>
</ul>
<form [formGroup]="searchForm" class="form-inline">
<div class="input-group mb-2">
<input style="width: 350px;" formControlName="searchText" type="text" class="form-control" i18n-placeholder="Search Assets Placeholder Text" placeholder="Search asset">
<div class="input-group-append">
<button [disabled]="!searchForm.get('searchText')?.value.length" class="btn btn-secondary" type="button" (click)="searchForm.get('searchText')?.setValue('');" autocomplete="off" i18n="Search Clear Button">Clear</button>
</div>
</div>
</form>
<div class="clearfix"></div>
<router-outlet></router-outlet>
</div>
<br>

View File

@@ -0,0 +1,9 @@
ul {
margin-bottom: 20px;
float: left;
}
form {
float: right;
}

View File

@@ -0,0 +1,23 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-assets-nav',
templateUrl: './assets-nav.component.html',
styleUrls: ['./assets-nav.component.scss']
})
export class AssetsNavComponent implements OnInit {
activeTab = 0;
searchForm: FormGroup;
constructor(
private formBuilder: FormBuilder,
) { }
ngOnInit(): void {
this.searchForm = this.formBuilder.group({
searchText: [{ value: '', disabled: true }, Validators.required]
});
}
}

View File

@@ -0,0 +1,52 @@
<ng-container *ngIf="(assets$ | async) as filteredAssets; else isLoading">
<table class="table table-borderless table-striped">
<thead>
<th class="td-name" i18n="Asset name header">Name</th>
<th i18n="Asset ticker header">Ticker</th>
<th class="d-none d-md-block" i18n="Asset Issuer Domain header">Issuer domain</th>
<th i18n="Asset ID header">Asset ID</th>
</thead>
<tbody>
<tr *ngFor="let asset of filteredAssets; trackBy: trackByAsset">
<td class="td-name"><a [routerLink]="['/assets/asset/' | relativeUrl, asset.asset_id]">{{ asset.name }}</a></td>
<td>{{ asset.ticker }}</td>
<td class="d-none d-md-block">{{ asset.entity && asset.entity.domain }}</td>
<td><a [routerLink]="['/assets/asset/' | relativeUrl, asset.asset_id]">{{ asset.asset_id | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="asset.asset_id"></app-clipboard></td>
</tr>
</tbody>
</table>
<br>
<ngb-pagination [collectionSize]="assets.length" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="5" [boundaryLinks]="true"></ngb-pagination>
</ng-container>
<ng-template #isLoading>
<table class="table table-borderless table-striped">
<thead>
<th i18n="Asset name header">Name</th>
<th i18n="Asset ticker header">Ticker</th>
<th class="d-none d-md-block" i18n="Asset Issuer Domain header">Issuer domain</th>
<th i18n="Asset ID header">Asset ID</th>
</thead>
<tbody>
<tr *ngFor="let dummy of [0,0,0,0,0,0,0,0,0,0]">
<td><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
<td class="d-none d-md-block"><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</ng-template>
<ng-template [ngIf]="error">
<div class="text-center">
<ng-container i18n="Asset data load error">Error loading assets data.</ng-container>
<br>
<i>{{ error.error }}</i>
</div>
</ng-template>

View File

@@ -0,0 +1,13 @@
.td-name {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.title-asset {
h1 {
line-height: 1;
margin: 0px;
padding-bottom: 10px;
}
}

View File

@@ -0,0 +1,168 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { AssetsService } from 'src/app/services/assets.service';
import { environment } from 'src/environments/environment';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { distinctUntilChanged, map, filter, mergeMap, tap, take } from 'rxjs/operators';
import { ActivatedRoute, Router } from '@angular/router';
import { merge, combineLatest, Observable } from 'rxjs';
import { AssetExtended } from 'src/app/interfaces/electrs.interface';
import { SeoService } from 'src/app/services/seo.service';
import { StateService } from 'src/app/services/state.service';
@Component({
selector: 'app-assets',
templateUrl: './assets.component.html',
styleUrls: ['./assets.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AssetsComponent implements OnInit {
nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
assets: AssetExtended[];
assetsCache: AssetExtended[];
searchForm: FormGroup;
assets$: Observable<AssetExtended[]>;
page = 1;
error: any;
itemsPerPage: number;
contentSpace = window.innerHeight - (250 + 200);
fiveItemsPxSize = 250;
constructor(
private assetsService: AssetsService,
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private router: Router,
private seoService: SeoService,
private stateService: StateService,
) { }
ngOnInit() {
this.seoService.setTitle($localize`:@@ee8f8008bae6ce3a49840c4e1d39b4af23d4c263:Assets`);
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
this.searchForm = this.formBuilder.group({
searchText: [{ value: '', disabled: true }, Validators.required]
});
this.assets$ = combineLatest([
this.assetsService.getAssetsJson$,
this.route.queryParams,
])
.pipe(
take(1),
mergeMap(([assets, qp]) => {
this.assets = Object.values(assets);
if (this.stateService.network === 'liquid') {
// @ts-ignore
this.assets.push({
name: 'Liquid Bitcoin',
ticker: 'L-BTC',
asset_id: this.nativeAssetId,
});
} else if (this.stateService.network === 'liquidtestnet') {
// @ts-ignore
this.assets.push({
name: 'Test Liquid Bitcoin',
ticker: 'tL-BTC',
asset_id: this.nativeAssetId,
});
}
this.assets = this.assets.sort((a: any, b: any) => a.name.localeCompare(b.name));
this.assetsCache = this.assets;
this.searchForm.get('searchText').enable();
if (qp.search) {
this.searchForm.get('searchText').setValue(qp.search, { emitEvent: false });
}
return merge(
this.searchForm.get('searchText').valueChanges
.pipe(
distinctUntilChanged(),
tap((text) => {
this.page = 1;
this.searchTextChanged(text);
})
),
this.route.queryParams
.pipe(
filter((queryParams) => {
const newPage = parseInt(queryParams.page, 10);
if (newPage !== this.page || queryParams.search !== this.searchForm.get('searchText').value) {
return true;
}
return false;
}),
map((queryParams) => {
if (queryParams.page) {
const newPage = parseInt(queryParams.page, 10);
this.page = newPage;
} else {
this.page = 1;
}
if (this.searchForm.get('searchText').value !== (queryParams.search || '')) {
this.searchTextChanged(queryParams.search);
}
if (queryParams.search) {
this.searchForm.get('searchText').setValue(queryParams.search, { emitEvent: false });
return queryParams.search;
}
return '';
})
),
);
}),
map((searchText) => {
const start = (this.page - 1) * this.itemsPerPage;
if (searchText.length ) {
const filteredAssets = this.assetsCache.filter((asset) => asset.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1
|| (asset.ticker || '').toLowerCase().indexOf(searchText.toLowerCase()) > -1);
this.assets = filteredAssets;
return filteredAssets.slice(start, this.itemsPerPage + start);
} else {
this.assets = this.assetsCache;
return this.assets.slice(start, this.itemsPerPage + start);
}
})
);
}
pageChange(page: number) {
const queryParams = { page: page, search: this.searchForm.get('searchText').value };
if (queryParams.search === '') {
queryParams.search = null;
}
if (queryParams.page === 1) {
queryParams.page = null;
}
this.page = -1;
this.router.navigate([], {
relativeTo: this.route,
queryParams: queryParams,
queryParamsHandling: 'merge',
});
}
searchTextChanged(text: string) {
const queryParams = { search: text, page: 1 };
if (queryParams.search === '') {
queryParams.search = null;
}
if (queryParams.page === 1) {
queryParams.page = null;
}
this.router.navigate([], {
relativeTo: this.route,
queryParams: queryParams,
queryParamsHandling: 'merge',
});
}
trackByAsset(index: number, asset: any) {
return asset.asset_id;
}
}