From cd1a0208df35a5fa38853a78779d19864f3b9e72 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Fri, 22 Oct 2021 17:20:36 -0500 Subject: [PATCH] Implement create item Adds Zcash price feed from CoinGecko --- backend/app.js | 27 +++++++++++ package-lock.json | 11 +++++ package.json | 1 + src/app/app.module.ts | 11 +++-- src/app/fullnode.service.ts | 26 ++++++----- .../item-create/item-create.component.html | 21 +++++++++ .../item-create/item-create.component.ts | 42 ++++++++++++++++++ .../items/item-list/item-list.component.css | 19 ++++++++ .../items/item-list/item-list.component.html | 30 +++++++++---- .../items/item-list/item-list.component.ts | 33 ++++++++++++-- src/app/items/item.model.ts | 2 +- src/app/items/items.service.ts | 13 ++++++ src/app/viewer/viewer.component.html | 2 - src/assets/zec.png | Bin 0 -> 1791 bytes 14 files changed, 207 insertions(+), 31 deletions(-) create mode 100644 src/app/items/item-create/item-create.component.html create mode 100644 src/app/items/item-create/item-create.component.ts create mode 100644 src/app/items/item-list/item-list.component.css create mode 100644 src/assets/zec.png diff --git a/backend/app.js b/backend/app.js index cde8294..b4e573c 100644 --- a/backend/app.js +++ b/backend/app.js @@ -7,6 +7,7 @@ const ownermodel = require('./models/owner'); const itemmodel = require('./models/item'); const mongoose = require('mongoose'); const stdrpc = require('stdrpc'); +const CoinGecko = require('coingecko-api'); //const RequestIP = require('@supercharge/request-ip'); var db = require('./config/db'); @@ -23,6 +24,8 @@ const rpc = stdrpc({ password: fullnode.password }); +const CoinGeckoClient = new CoinGecko(); + app.use(bodyparser.json()); 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; diff --git a/package-lock.json b/package-lock.json index 7b2e722..87e5d24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@angular/router": "~12.2.0", "@supercharge/request-ip": "^1.1.2", "angular-local-storage": "^0.7.1", + "coingecko-api": "^1.0.10", "easyqrcodejs": "^4.4.6", "rxjs": "~6.6.0", "tslib": "^2.3.0", @@ -4127,6 +4128,11 @@ "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": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -18668,6 +18674,11 @@ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "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": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", diff --git a/package.json b/package.json index 32d29fe..1df6e58 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@angular/router": "~12.2.0", "@supercharge/request-ip": "^1.1.2", "angular-local-storage": "^0.7.1", + "coingecko-api": "^1.0.10", "easyqrcodejs": "^4.4.6", "rxjs": "~6.6.0", "tslib": "^2.3.0", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 2426277..5dea57f 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,6 +1,6 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; -import { FormsModule } from '@angular/forms'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatInputModule } from '@angular/material/input'; import { MatCardModule } from '@angular/material/card'; import { MatButtonModule } from '@angular/material/button'; @@ -15,6 +15,7 @@ import { HeaderComponent } from './header/header.component'; import { ViewerComponent } from './viewer/viewer.component'; import { LoginComponent } from './login/login.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 { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @@ -24,13 +25,15 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; HeaderComponent, ViewerComponent, ItemListComponent, - LoginComponent + LoginComponent, + ItemCreateComponent //NameDialogComponent, ], imports: [ BrowserModule, AppRoutingModule, FormsModule, + ReactiveFormsModule, HttpClientModule, MatInputModule, MatCardModule, @@ -41,7 +44,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; BrowserAnimationsModule ], providers: [], - bootstrap: [AppComponent]//, - //entryComponents: [NameDialogComponent] + bootstrap: [AppComponent], + entryComponents: [ItemCreateComponent] }) export class AppModule { } diff --git a/src/app/fullnode.service.ts b/src/app/fullnode.service.ts index 88a0671..09a711b 100644 --- a/src/app/fullnode.service.ts +++ b/src/app/fullnode.service.ts @@ -7,19 +7,22 @@ import {UserService} from './user.service'; @Injectable({providedIn: 'root'}) 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 = new BehaviorSubject(this.dataStore.height); private _memoUpdated: BehaviorSubject = new BehaviorSubject(this.dataStore.memoList); private _addrUpdated: BehaviorSubject = new BehaviorSubject(this.dataStore.addr); + private _priceUpdated: BehaviorSubject = new BehaviorSubject(this.dataStore.price); public readonly addrUpdate: Observable = this._addrUpdated.asObservable(); public readonly heightUpdate: Observable = this._heightUpdated.asObservable(); public readonly memoUpdate: Observable = this._memoUpdated.asObservable(); + public readonly priceUpdate: Observable = this._priceUpdated.asObservable(); private UserSub: Subscription = new Subscription(); constructor(private http: HttpClient, public userService: UserService){ this.getAddr(); this.getHeight(); this.getMemos(); + this.getPrice(); } getHeight(){ @@ -32,9 +35,16 @@ export class FullnodeService{ return obs; } - //getHeightUpdateListener() { - //return this.heightUpdated; - //} + getPrice(){ + 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) { var str = ''; @@ -81,9 +91,6 @@ export class FullnodeService{ }); } - //getMemoUpdateListener() { - //return this.memoUpdated; - //} getAddr() { let obs = this.http.get<{message: string, addr: string}>('http://localhost:3000/api/getaddr'); @@ -95,9 +102,4 @@ export class FullnodeService{ return obs; } - - //getAddrUpdateListener() { - //return this.addrUpdated; - //} - } diff --git a/src/app/items/item-create/item-create.component.html b/src/app/items/item-create/item-create.component.html new file mode 100644 index 0000000..318936b --- /dev/null +++ b/src/app/items/item-create/item-create.component.html @@ -0,0 +1,21 @@ +

Add item

+ + + + + + + + + + +
+
Use only numbers
+
+
+
+ + + + + diff --git a/src/app/items/item-create/item-create.component.ts b/src/app/items/item-create/item-create.component.ts new file mode 100644 index 0000000..508d017 --- /dev/null +++ b/src/app/items/item-create/item-create.component.ts @@ -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, + @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(); + } +} diff --git a/src/app/items/item-list/item-list.component.css b/src/app/items/item-list/item-list.component.css new file mode 100644 index 0000000..03c8440 --- /dev/null +++ b/src/app/items/item-list/item-list.component.css @@ -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; +} diff --git a/src/app/items/item-list/item-list.component.html b/src/app/items/item-list/item-list.component.html index 2bcb03d..2f0c6dc 100644 --- a/src/app/items/item-list/item-list.component.html +++ b/src/app/items/item-list/item-list.component.html @@ -1,10 +1,24 @@

Items!

- - - - {{item.name}} - -

{{item.description}}

-
-
+
+
+ + + + + + + +
{{item.name}} +

{{item.cost | currency: 'USD'}}

+

{{priceUpdate | async}}

+
+
+ +

{{item.description}}

+
+
+
+

No items yet!

+
+ diff --git a/src/app/items/item-list/item-list.component.ts b/src/app/items/item-list/item-list.component.ts index 5620e7a..0b12752 100644 --- a/src/app/items/item-list/item-list.component.ts +++ b/src/app/items/item-list/item-list.component.ts @@ -1,28 +1,36 @@ import { Component, OnInit } from '@angular/core'; +import { MatDialog, MatDialogConfig} from '@angular/material/dialog'; import { Observable } from 'rxjs'; import { Item } from '../item.model'; import { Owner } from '../../owner.model'; +import { FullnodeService } from '../../fullnode.service'; import { UserService } from '../../user.service'; import { ItemService } from '../items.service'; +import { ItemCreateComponent } from '../item-create/item-create.component'; @Component({ 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[] = []; private owner: Owner = {_id: '', name: '', address: ''}; public ownerUpdate: Observable; public itemsUpdate: Observable; + public priceUpdate: Observable; constructor( - itemService: ItemService, - userService: UserService + public itemService: ItemService, + userService: UserService, + public fullnodeService: FullnodeService, + private dialog: MatDialog ) { this.ownerUpdate = userService.ownerUpdate; this.itemsUpdate = itemService.itemsUpdated; + this.priceUpdate = fullnodeService.priceUpdate; this.ownerUpdate.subscribe((owner) => { this.owner = owner; itemService.getItems(this.owner.address); @@ -34,5 +42,22 @@ export class ItemListComponent{ 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); + }); + } } diff --git a/src/app/items/item.model.ts b/src/app/items/item.model.ts index 2805009..3e239f5 100644 --- a/src/app/items/item.model.ts +++ b/src/app/items/item.model.ts @@ -1,5 +1,5 @@ export interface Item { - id: string; + _id?: string; name: string; description: string; cost: number; diff --git a/src/app/items/items.service.ts b/src/app/items/items.service.ts index 1db5d59..f1e3ee8 100644 --- a/src/app/items/items.service.ts +++ b/src/app/items/items.service.ts @@ -25,5 +25,18 @@ export class ItemService{ 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; } } diff --git a/src/app/viewer/viewer.component.html b/src/app/viewer/viewer.component.html index d0739d4..8c79dc0 100644 --- a/src/app/viewer/viewer.component.html +++ b/src/app/viewer/viewer.component.html @@ -1,7 +1,5 @@

{{message}}

-
- {{addrUpdate | async}}
diff --git a/src/assets/zec.png b/src/assets/zec.png new file mode 100644 index 0000000000000000000000000000000000000000..1cc824582b0b7ae5b8491ac2b1b62044531ec87e GIT binary patch literal 1791 zcmV zaB^>EX>4U6ba`-PAZ2)IW&i+q+U=HYk|QS!hW~Sl905rPiQ~|GROJRaeqLzowWnq` zv)RAXR0AzQ5>6(bUfZq-8IFu>-%G#F?yE=Ii1K5 z30k>7JsQY2kKG8ehfSe9z#O6f!L3tdK3vC%jy2Uj^$k3 zUbk@8t?kok_ZwD~#5ijvOQ;h^vs}=3j>&LteK|TCm$go>ThGVu@R-W*iHDHHUHk2& zte2!NIcmlnY)TaUB#v6-KHnIC3@*U)a?WA@ds#*?yo$clatgGJ#c z3wm0GCYu!LfX~tpDgr6b8ICWs?Urq?k;8H)Ol5*HTJiH=C46PlugaV&mx#h6XX;`_ zywVVEEOM*HwDi0>R#MYTc@M|~R)A3VF*`O`<8`aJlzzmPY|x9YqP>%wtl#UGq6FIt zXGnk`W~35jipFe2I7kN|`0Ff*FAyMA?r@TmNCq1u0Za$1G1_xk`Ih6B1PPTa1~x@1 zV5OvpA4`rHsw$dPHLIy>(V8WvfDFso^15!Ti6v9ZX69C`x_EN+?B?#pYvCfejkO>h zidoR5X8VIG4 zhK7-c4IgFHiCCL5)6|)#O`m1fCp9{0`?356H9FLIlUnoaNexo7yA3;Dpky%vF-`>H zwg@1gc`=JlDe)q=n8m`#3S~H{i%lnlVCEDg4Pu@2WcP#IZ*dDb{gNAhm0Vcp{*c_} z7KXlX`-ECwKih2+yR&eeX%u81#)nt-x_(7#cy#`>{Z{m?=v&dZqHjh2PtnAkjX(0? zUm39<0fpAhbN~PWglR)VP)S2WAaHVTW@&6?004NLeUUv#!$2IxUsI)8Dh>t_amY}e zEQ*RaY88r5A=C=3I+%}sL6e3g#l=x@EjakGSaoo5*44pP5ClI!+}xZLU8KbSC509- z9vt`M-Mz=%JAkW~70v1x2Q=L_Q;E2c$*u~aSA@|I9%Ja16w8J%U`JjGoPf_T3Yl7=-mb`u3MVC2VCv|15buzO0E>7$>sCF`x$*x z7U;VL!fRe{&3&9c0BPzfc>^3A0wV>=UiW!-S9@>&o@w^?1A63gs#e@mkpKVy24YJ` zL;(K){{a7>y{D4^000SaNLh0L04^f{04^f|c%?sf00007bV*G`2jvPD6fhtB{s7qk z000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HM@dakSAh-}0005DNkl+L_x&DMzOLHv{h_;1B(=S0^dLp!OGG`A=pbQwNWe;5jBW~s0iZS z+-y!HESs5_owKpX52r|A=bJtM%$Ye0!4wbja4f*HN#Fo@08R-}N#G1919@OBO!$$2 z%T*H?{p%pn2I1X4-p9j&`{_0;c){oQU67flJw9qhS|jZ=)AJVmU&D*dg6{*Ht_Pi$ z5^LIGCqS2L@EGuH0bc=quED*)wFUeLjJO6TfUE`l2~4{NM}SqJWB?a|C5=_PD1+Am zmw+{yq*Dz6PnL9N#TKV(1CA}=oGe7=KCfGRegnoed|r@CgK2@KG{6J0Urp)W(J0pm z;LrlT@VQhv(Xz$ocP*bMq)M55PHTYMr42HrI}@VkHC#i hAarm=!_5K*)(@|7Dy(_|6~O=i002ovPDHLkV1h?ZF(&{3 literal 0 HcmV?d00001