Implement create item

Adds Zcash price feed from CoinGecko
This commit is contained in:
Rene Vergara 2021-10-22 17:20:36 -05:00
parent 22c89b9d4f
commit cd1a0208df
14 changed files with 207 additions and 31 deletions

View file

@ -7,6 +7,7 @@ const ownermodel = require('./models/owner');
const itemmodel = require('./models/item'); const itemmodel = require('./models/item');
const mongoose = require('mongoose'); const mongoose = require('mongoose');
const stdrpc = require('stdrpc'); const stdrpc = require('stdrpc');
const CoinGecko = require('coingecko-api');
//const RequestIP = require('@supercharge/request-ip'); //const RequestIP = require('@supercharge/request-ip');
var db = require('./config/db'); var db = require('./config/db');
@ -23,6 +24,8 @@ const rpc = stdrpc({
password: fullnode.password password: fullnode.password
}); });
const CoinGeckoClient = new CoinGecko();
app.use(bodyparser.json()); app.use(bodyparser.json());
app.use((req, res, next) => { app.use((req, res, next) => {
@ -194,4 +197,28 @@ app.get('/api/getitems', (req, res, next) => {
}); });
}); });
app.post('/api/item', (req, res, next) => {
console.log('Post: /api/item');
const item = new itemmodel(req.body.item);
item.save();
res.status(201).json({
message: 'Item added'
});
});
app.get('/api/price', (req, res, next) => {
console.log('Get /api/price');
CoinGeckoClient.simple.price({
ids: ['zcash'],
vs_currencies: ['usd']
}).
then((data) => {
res.status(200).json({
message: 'price found!',
price: data.data.zcash.usd
});
});
});
module.exports = app; module.exports = app;

11
package-lock.json generated
View file

@ -20,6 +20,7 @@
"@angular/router": "~12.2.0", "@angular/router": "~12.2.0",
"@supercharge/request-ip": "^1.1.2", "@supercharge/request-ip": "^1.1.2",
"angular-local-storage": "^0.7.1", "angular-local-storage": "^0.7.1",
"coingecko-api": "^1.0.10",
"easyqrcodejs": "^4.4.6", "easyqrcodejs": "^4.4.6",
"rxjs": "~6.6.0", "rxjs": "~6.6.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
@ -4127,6 +4128,11 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/coingecko-api": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/coingecko-api/-/coingecko-api-1.0.10.tgz",
"integrity": "sha512-7YLLC85+daxAw5QlBWoHVBVpJRwoPr4HtwanCr8V/WRjoyHTa1Lb9DQAvv4MDJZHiz4no6HGnDQnddtjV35oRA=="
},
"node_modules/collection-visit": { "node_modules/collection-visit": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
@ -18668,6 +18674,11 @@
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"dev": true "dev": true
}, },
"coingecko-api": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/coingecko-api/-/coingecko-api-1.0.10.tgz",
"integrity": "sha512-7YLLC85+daxAw5QlBWoHVBVpJRwoPr4HtwanCr8V/WRjoyHTa1Lb9DQAvv4MDJZHiz4no6HGnDQnddtjV35oRA=="
},
"collection-visit": { "collection-visit": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",

View file

@ -22,6 +22,7 @@
"@angular/router": "~12.2.0", "@angular/router": "~12.2.0",
"@supercharge/request-ip": "^1.1.2", "@supercharge/request-ip": "^1.1.2",
"angular-local-storage": "^0.7.1", "angular-local-storage": "^0.7.1",
"coingecko-api": "^1.0.10",
"easyqrcodejs": "^4.4.6", "easyqrcodejs": "^4.4.6",
"rxjs": "~6.6.0", "rxjs": "~6.6.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",

View file

@ -1,6 +1,6 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@ -15,6 +15,7 @@ import { HeaderComponent } from './header/header.component';
import { ViewerComponent } from './viewer/viewer.component'; import { ViewerComponent } from './viewer/viewer.component';
import { LoginComponent } from './login/login.component'; import { LoginComponent } from './login/login.component';
import { ItemListComponent } from './items/item-list/item-list.component'; import { ItemListComponent } from './items/item-list/item-list.component';
import { ItemCreateComponent } from './items/item-create/item-create.component';
//import { NameDialogComponent } from './namedialog/namedialog.component'; //import { NameDialogComponent } from './namedialog/namedialog.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@ -24,13 +25,15 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
HeaderComponent, HeaderComponent,
ViewerComponent, ViewerComponent,
ItemListComponent, ItemListComponent,
LoginComponent LoginComponent,
ItemCreateComponent
//NameDialogComponent, //NameDialogComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
AppRoutingModule, AppRoutingModule,
FormsModule, FormsModule,
ReactiveFormsModule,
HttpClientModule, HttpClientModule,
MatInputModule, MatInputModule,
MatCardModule, MatCardModule,
@ -41,7 +44,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
BrowserAnimationsModule BrowserAnimationsModule
], ],
providers: [], providers: [],
bootstrap: [AppComponent]//, bootstrap: [AppComponent],
//entryComponents: [NameDialogComponent] entryComponents: [ItemCreateComponent]
}) })
export class AppModule { } export class AppModule { }

View file

@ -7,19 +7,22 @@ import {UserService} from './user.service';
@Injectable({providedIn: 'root'}) @Injectable({providedIn: 'root'})
export class FullnodeService{ export class FullnodeService{
private dataStore: { height: number, memoList: string[], addr: string } = { height: 0, memoList: [], addr: '' }; private dataStore: { height: number, memoList: string[], addr: string, price: number } = { height: 0, memoList: [], addr: '', price:0 };
private _heightUpdated: BehaviorSubject<number> = new BehaviorSubject(this.dataStore.height); private _heightUpdated: BehaviorSubject<number> = new BehaviorSubject(this.dataStore.height);
private _memoUpdated: BehaviorSubject<string[]> = new BehaviorSubject(this.dataStore.memoList); private _memoUpdated: BehaviorSubject<string[]> = new BehaviorSubject(this.dataStore.memoList);
private _addrUpdated: BehaviorSubject<string> = new BehaviorSubject(this.dataStore.addr); private _addrUpdated: BehaviorSubject<string> = new BehaviorSubject(this.dataStore.addr);
private _priceUpdated: BehaviorSubject<number> = new BehaviorSubject(this.dataStore.price);
public readonly addrUpdate: Observable<string> = this._addrUpdated.asObservable(); public readonly addrUpdate: Observable<string> = this._addrUpdated.asObservable();
public readonly heightUpdate: Observable<number> = this._heightUpdated.asObservable(); public readonly heightUpdate: Observable<number> = this._heightUpdated.asObservable();
public readonly memoUpdate: Observable<string[]> = this._memoUpdated.asObservable(); public readonly memoUpdate: Observable<string[]> = this._memoUpdated.asObservable();
public readonly priceUpdate: Observable<number> = this._priceUpdated.asObservable();
private UserSub: Subscription = new Subscription(); private UserSub: Subscription = new Subscription();
constructor(private http: HttpClient, public userService: UserService){ constructor(private http: HttpClient, public userService: UserService){
this.getAddr(); this.getAddr();
this.getHeight(); this.getHeight();
this.getMemos(); this.getMemos();
this.getPrice();
} }
getHeight(){ getHeight(){
@ -32,9 +35,16 @@ export class FullnodeService{
return obs; return obs;
} }
//getHeightUpdateListener() { getPrice(){
//return this.heightUpdated; let obs = this.http.get<{message: string, price: number}>('http://localhost:3000/api/price');
//} obs.subscribe((PriceData) => {
this.dataStore.price = PriceData.price;
console.log(this.dataStore.price);
this._priceUpdated.next(Object.assign({},this.dataStore).price);
});
return obs;
}
hexToString(hexString: string) { hexToString(hexString: string) {
var str = ''; var str = '';
@ -81,9 +91,6 @@ export class FullnodeService{
}); });
} }
//getMemoUpdateListener() {
//return this.memoUpdated;
//}
getAddr() { getAddr() {
let obs = this.http.get<{message: string, addr: string}>('http://localhost:3000/api/getaddr'); let obs = this.http.get<{message: string, addr: string}>('http://localhost:3000/api/getaddr');
@ -95,9 +102,4 @@ export class FullnodeService{
return obs; return obs;
} }
//getAddrUpdateListener() {
//return this.addrUpdated;
//}
} }

View file

@ -0,0 +1,21 @@
<h2 mat-dialog-title>Add item</h2>
<mat-dialog-content [formGroup]="form">
<mat-form-field>
<input matInput placeholder="Item" formControlName="name">
</mat-form-field>
<mat-form-field>
<textarea matInput placeholder="Description" formControlName="description"></textarea>
</mat-form-field>
<mat-form-field>
<input matInput type="number" placeholder="Price in USD" formControlName="cost">
<div *ngIf="!form.controls['cost'].valid && form.controls['cost'].touched">
<div *ngIf="form.controls['cost'].errors?.pattern">Use only numbers</div>
</div>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-raised-button (click)="close()">Close</button>
<button mat-raised-button color="primary" (click)="save()">Save</button>
</mat-dialog-actions>

View file

@ -0,0 +1,42 @@
import { Inject, Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { FormBuilder, Validators, FormGroup, FormControl } from '@angular/forms';
import { Item } from '../item.model';
@Component({
selector: 'app-item-create',
templateUrl: './item-create.component.html'
})
export class ItemCreateComponent implements OnInit {
form: FormGroup;
numberRegEx = /\d*\.?\d{1,2}/;
constructor(
private fb: FormBuilder,
private dialogRef: MatDialogRef<ItemCreateComponent>,
@Inject(MAT_DIALOG_DATA) {name, description, cost, user}:Item
){
this.form = fb.group({
name: [null, Validators.required],
description: [null, Validators.required],
cost: new FormControl('', {
validators: [Validators.required, Validators.pattern(this.numberRegEx)],
updateOn: "blur"
})
});
}
ngOnInit () {
}
save() {
this.dialogRef.close(this.form.value);
}
close () {
this.dialogRef.close();
}
}

View file

@ -0,0 +1,19 @@
* {
font-family: 'Roboto-Mono', monospace;
}
.spacer{
flex: 1 1 auto;
}
img.icon{
margin-bottom: -1px;
}
div.card{
margin-top: 3px;
}
p.price{
margin: 0px;
}

View file

@ -1,10 +1,24 @@
<h1>Items!</h1> <h1>Items!</h1>
<mat-accordion *ngIf="items.length > 0"> <div *ngIf="items.length > 0">
<mat-expansion-panel *ngFor="let item of items"> <div class="card" *ngFor="let item of items">
<mat-expansion-panel-header> <mat-card>
{{item.name}} <mat-card-title>
</mat-expansion-panel-header> <table cellspacing="0" width="100%">
<p>{{item.description}}</p> <tr>
</mat-expansion-panel> <td>{{item.name}}</td>
</mat-accordion> <td align="right">
<p class="price">{{item.cost | currency: 'USD'}}</p>
<p class="price"><img class="icon" src="/assets/zec.png" width="12px" />{{priceUpdate | async}}</p>
</td>
</tr>
</table>
</mat-card-title>
<mat-card-subtitle>
<p>{{item.description}}</p>
</mat-card-subtitle>
</mat-card>
</div>
</div>
<p *ngIf = "items.length <= 0">No items yet!</p> <p *ngIf = "items.length <= 0">No items yet!</p>
<br>
<button mat-raised-button (click)="openDialog()">Add item</button>

View file

@ -1,28 +1,36 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { MatDialog, MatDialogConfig} from '@angular/material/dialog';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Item } from '../item.model'; import { Item } from '../item.model';
import { Owner } from '../../owner.model'; import { Owner } from '../../owner.model';
import { FullnodeService } from '../../fullnode.service';
import { UserService } from '../../user.service'; import { UserService } from '../../user.service';
import { ItemService } from '../items.service'; import { ItemService } from '../items.service';
import { ItemCreateComponent } from '../item-create/item-create.component';
@Component({ @Component({
selector: 'app-item-list', selector: 'app-item-list',
templateUrl: './item-list.component.html' templateUrl: './item-list.component.html',
styleUrls: ['./item-list.component.css']
}) })
export class ItemListComponent{ export class ItemListComponent implements OnInit{
public items: Item[] = []; public items: Item[] = [];
private owner: Owner = {_id: '', name: '', address: ''}; private owner: Owner = {_id: '', name: '', address: ''};
public ownerUpdate: Observable<Owner>; public ownerUpdate: Observable<Owner>;
public itemsUpdate: Observable<Item[]>; public itemsUpdate: Observable<Item[]>;
public priceUpdate: Observable<number>;
constructor( constructor(
itemService: ItemService, public itemService: ItemService,
userService: UserService userService: UserService,
public fullnodeService: FullnodeService,
private dialog: MatDialog
) { ) {
this.ownerUpdate = userService.ownerUpdate; this.ownerUpdate = userService.ownerUpdate;
this.itemsUpdate = itemService.itemsUpdated; this.itemsUpdate = itemService.itemsUpdated;
this.priceUpdate = fullnodeService.priceUpdate;
this.ownerUpdate.subscribe((owner) => { this.ownerUpdate.subscribe((owner) => {
this.owner = owner; this.owner = owner;
itemService.getItems(this.owner.address); itemService.getItems(this.owner.address);
@ -34,5 +42,22 @@ export class ItemListComponent{
ngOnInit(){ ngOnInit(){
} }
openDialog(){
const dialogConfig = new MatDialogConfig();
dialogConfig.disableClose = true;
dialogConfig.autoFocus = true;
dialogConfig.data = {name: '' , user: '', description: '', cost: 0}
const dialogRef = this.dialog.open(ItemCreateComponent, dialogConfig);
dialogRef.afterClosed().subscribe((val) => {
//TODO connect to Item service
var item:Item = {name: val.name, description: val.description, cost: val.cost, user: this.owner.address};
this.itemService.addItem(item);
this.itemService.getItems(this.owner.address);
});
}
} }

View file

@ -1,5 +1,5 @@
export interface Item { export interface Item {
id: string; _id?: string;
name: string; name: string;
description: string; description: string;
cost: number; cost: number;

View file

@ -25,5 +25,18 @@ export class ItemService{
console.log('No items found'); console.log('No items found');
} }
}); });
return obs;
}
addItem(item: Item) {
const params = new HttpParams().append('item', JSON.stringify(item));
let obs = this.http.post<{message: string}>('http://localhost:3000/api/item', { item: item });
obs.subscribe((ItemResponse) => {
console.log('Item added');
});
return obs;
} }
} }

View file

@ -1,7 +1,5 @@
<app-header></app-header> <app-header></app-header>
<div align="center"> <div align="center">
<h1>{{message}}</h1> <h1>{{message}}</h1>
<br>
{{addrUpdate | async}}
</div> </div>
<app-item-list></app-item-list> <app-item-list></app-item-list>

BIN
src/assets/zec.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB