Commit f9901e15 authored by Michel Schudel's avatar Michel Schudel

Merge branch 'frontend-improvements'

parents 9038da8d 4eac6742
.gradle
/build/
!gradle/wrapper/gradle-wrapper.jar
/*.json
### STS ###
.apt_generated
......
......@@ -33,17 +33,14 @@ processResources {
// configure gradle-node-plugin
node {
//version = '8.9.4'
//npmVersion = '5.6.0'
download = true
workDir = file("${project.projectDir}/frontend/node")
download = false
npmWorkDir = file("${project.projectDir}/frontend")
nodeModulesDir = file("${project.projectDir}/frontend")
}
// clean node/node_modules/dist
task npmClean(type: Delete) {
task frontendClean(type: Delete) {
final def webDir = "${rootDir}/frontend"
delete "${webDir}/node"
delete "${webDir}/node_modules"
delete "${webDir}/dist"
delete "${webDir}/coverage"
......@@ -51,7 +48,7 @@ task npmClean(type: Delete) {
}
clean.dependsOn(npmClean)
clean.dependsOn(frontendClean)
build.dependsOn(npm_install)
build.dependsOn(npm_run_build)
......
......@@ -19,7 +19,7 @@
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"styles": [
"styles.css"
"styles.scss"
],
"scripts": [],
"environmentSource": "environments/environment.ts",
......@@ -54,7 +54,8 @@
}
},
"defaults": {
"styleExt": "css",
"component": {}
"styleExt": "scss",
"component": {
}
}
}
......@@ -8,6 +8,7 @@
# dependencies
/node_modules
/node
# IDEs and editors
/.idea
......
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo() {
return browser.get('/');
}
getParagraphText() {
return element(by.css('app-root h1')).getText();
}
}
......@@ -1033,6 +1033,23 @@
"hoek": "2.16.3"
}
},
"bootstrap": {
"version": "4.0.0-beta.3",
"resolved": "https://npmjs.build.edubase.malmberg.nl/repository/npm/bootstrap/-/bootstrap-4.0.0-beta.3.tgz",
"integrity": "sha512-/Qe1Q2d1muLEZRX2iCteMQHZBBAm6ZIjJ9FcBYK/xLr05+HvDtBOVBN+Cz7mCNZuy0zr+y5artZHM05W7mIz6g==",
"dev": true
},
"bootswatch": {
"version": "4.0.0-beta.3",
"resolved": "https://npmjs.build.edubase.malmberg.nl/repository/npm/bootswatch/-/bootswatch-4.0.0-beta.3.tgz",
"integrity": "sha1-Uznc4FXBAR8pFGc2SwoliBgOjDc=",
"dev": true,
"requires": {
"bootstrap": "4.0.0-beta.3",
"jquery": "3.2.1",
"popper.js": "1.12.9"
}
},
"brace-expansion": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
......@@ -5865,6 +5882,12 @@
"integrity": "sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4=",
"dev": true
},
"jquery": {
"version": "3.2.1",
"resolved": "https://npmjs.build.edubase.malmberg.nl/repository/npm/jquery/-/jquery-3.2.1.tgz",
"integrity": "sha1-XE2d5lKvbNCncBVKYxu6ErAVx4c=",
"dev": true
},
"js-base64": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.0.tgz",
......@@ -7858,6 +7881,12 @@
"find-up": "2.1.0"
}
},
"popper.js": {
"version": "1.12.9",
"resolved": "https://npmjs.build.edubase.malmberg.nl/repository/npm/popper.js/-/popper.js-1.12.9.tgz",
"integrity": "sha1-DfvC3/lsRRuzMu3Pz6r1ZtMx1bM=",
"dev": true
},
"portfinder": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.13.tgz",
......
......@@ -4,10 +4,10 @@
"license": "MIT",
"scripts": {
"ng": "ng",
"start": "ng serve",
"start": "ng serve --aot -pc proxy.conf.json --open",
"build": "ng build --prod",
"test": "ng test",
"lint": "ng lint",
"lint": "ng lint --type-check=true",
"e2e": "ng e2e"
},
"private": true,
......@@ -33,6 +33,7 @@
"@types/jasmine": "~2.8.3",
"@types/jasminewd2": "~2.0.2",
"@types/node": "~6.0.60",
"bootswatch": "^4.0.0-beta.3",
"codelyzer": "^4.0.1",
"jasmine-core": "~2.8.0",
"jasmine-spec-reporter": "~4.2.1",
......
{
"/api/*": {
"target": "http://localhost:8080",
"secure": false
}
}
import { Transaction } from './transaction';
export interface Block {
index: number;
timestamp: number;
transactions: Transaction[];
proof: number;
}
import { Block } from './block';
export interface Blockchain {
chain: Block[];
}
export * from './block';
export * from './blockchain';
export * from './transaction';
export * from './transaction-result';
export class TransactionResult {
export interface TransactionResult {
message: string;
}
export class Transaction {
export interface Transaction {
id: string;
from: string;
to: string;
......
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { TransactionsComponent } from './transactions/transactions.component';
import { TransactionComponent } from './transaction/transaction.component';
import { DashboardComponent } from './dashboard/dashboard.component';
const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'pendingtransactions', component: TransactionsComponent },
{ path: 'newtransaction', component: TransactionComponent },
{ path: 'dashboard', component: DashboardComponent }
];
@NgModule({
exports: [ RouterModule ],
imports: [ RouterModule.forRoot(routes) ]
})
export class AppRoutingModule {
}
<!--The content below is only a placeholder and can be replaced.-->
<div style="text-align:center">
<h1>
{{ title }}
</h1>
</div>
<nav>
<a routerLink="/dashboard">Dashboard</a>
<a routerLink="/pendingtransactions">Pending transactions</a>
<a routerLink="/newtransaction">New transaction</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>
<div class="container">
<div class="header">
<img src="../assets/images/coin-logo.png" alt="logo" />
<h1>CraftsCoin</h1>
</div>
<div>
<p class="lead">Node interface</p>
</div>
<nav class="mt-5">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link" [routerLink]="['transactions']" routerLinkActive="active">Pending transactions</a>
</li>
<li class="nav-item">
<a class="nav-link" [routerLink]="['blockchain']" routerLinkActive="active">Blockchain</a>
</li>
<li class="nav-item">
<a class="nav-link" [routerLink]="['peers']" routerLinkActive="active">Peers</a>
</li>
</ul>
</nav>
<div class="row">
<div class="col-md-8">
<router-outlet></router-outlet>
</div>
<div class="col-md-4">
<app-messages></app-messages>
</div>
</div>
</div>
nav {
margin-bottom: 2rem;
}
.header img {
float: left;
width: 100px;
height: 100px;
}
.header h1 {
position: relative;
top: 18px;
left: 10px;
}
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
it(`should have as title 'app'`, async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('app');
}));
it('should render title in a h1 tag', async(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
}));
});
......@@ -3,8 +3,6 @@ import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'CraftsCoin node interface';
}
export class AppComponent {}
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
import { TransactionsComponent } from './transactions/transactions.component';
import { TransactionComponent } from './transaction/transaction.component';
import { FormsModule } from '@angular/forms';
import {TransactionService} from "./transaction.service";
import { CreateTransactionComponent } from './transactions/create/create-transaction.component';
import { TransactionService } from './transactions/transaction.service';
import { MessagesComponent } from './messages/messages.component';
import { MessageService } from './message.service';
import { AppRoutingModule } from './/app-routing.module';
import { DashboardComponent } from './dashboard/dashboard.component';
import {HttpClientModule} from "@angular/common/http";
import {HttpClientInMemoryWebApiModule} from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
import { MessageService } from './messages/message.service';
import { BlockchainComponent } from './blockchain/blockchain.component';
import { PendingTransactionsComponent } from './transactions/pending/pending-transactions.component';
import { BlockchainService } from './blockchain/blockchain.service';
import { PeersComponent } from './peers/peers.component';
import { PeersService } from './peers/peers.service';
const routes: Routes = [
{ path: '', redirectTo: '/transactions/pending', pathMatch: 'full' },
{ path: 'transactions', component: PendingTransactionsComponent },
{ path: 'transactions/create', component: CreateTransactionComponent },
{ path: 'blockchain', component: BlockchainComponent },
{ path: 'peers', component: PeersComponent },
];
@NgModule({
declarations: [
AppComponent,
TransactionsComponent,
TransactionComponent,
PendingTransactionsComponent,
CreateTransactionComponent,
CreateTransactionComponent,
MessagesComponent,
DashboardComponent
BlockchainComponent,
PeersComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
FormsModule,
AppRoutingModule,
RouterModule.forRoot(routes, { useHash: true }),
HttpClientModule
// ,HttpClientInMemoryWebApiModule.forRoot(
// InMemoryDataService, { dataEncapsulation: false }
// )
],
providers: [TransactionService, MessageService],
providers: [TransactionService, MessageService, BlockchainService, PeersService],
bootstrap: [AppComponent]
})
export class AppModule { }
export class AppModule {
}
<h3 class="mb-4">Blockchain</h3>
<p>
<button (click)="mine()" class="btn btn-primary btn-lg">Mine!</button>
</p>
<div [@slide]="blockchain?.chain.length">
<div *ngFor="let block of blockchain?.chain; trackBy: trackByFn" class="block">
<h4 class="mb-3">
<span class="badge badge-primary">Block {{ block.index }}</span>
<span class="badge badge-secondary">{{ block.timestamp | date: 'mediumTime' }}</span>
<span class="badge badge-success">Proof {{ block.proof }}</span>
</h4>
<ng-container *ngIf="block.transactions.length === 0">No transactions.</ng-container>
<ng-container *ngIf="block.transactions.length > 0">
<div *ngFor="let transaction of block.transactions" class="transaction mt-3">
<p class="lead mb-0">{{ transaction.id }}</p>
<span class="badge badge-primary">from {{ transaction.from }}</span>
<span class="badge badge-secondary">to {{ transaction.to }}</span>
<span class="badge badge-info">{{ transaction.amount | currency: 'EUR' }}</span>
</div>
</ng-container>
</div>
</div>
.block {
border: 1px solid gray;
border-radius: 8px;
padding: 0.5rem 1rem;
display: block;
margin: 2rem 0;
overflow: hidden;
}
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { transition, trigger, useAnimation } from '@angular/animations';
import 'rxjs/add/observable/timer';
import 'rxjs/add/operator/switchMap';
import { Block, Blockchain } from '../api';
import { BlockchainService } from './blockchain.service';
import { slideAnimation } from '../slide.animation';
@Component({
selector: 'app-blockchain',
templateUrl: './blockchain.component.html',
styleUrls: ['./blockchain.component.scss'],
animations: [
trigger('slide', [
transition('* => *', [
useAnimation(slideAnimation),
]),
]),
],
})
export class BlockchainComponent implements OnInit, OnDestroy {
blockchain: Blockchain;
private subscription: Subscription;
constructor(private blockchainService: BlockchainService) {}
ngOnInit(): void {
this.subscription = Observable.timer(0, 5000)
.switchMap(() => this.blockchainService.getBlockchain())
.subscribe(blockchain => this.blockchain = blockchain);
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
trackByFn(index: number, block: Block): number {
return block.index;
}
mine(): void {
this.blockchainService.doMine().subscribe(block => this.blockchain.chain.unshift(block));
}
}
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/empty';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/catch';
import { MessageService } from '../messages/message.service';
import { Block, Blockchain } from '../api';
@Injectable()
export class BlockchainService {
constructor(private http: HttpClient, private messageService: MessageService) {}
getBlockchain(): Observable<Blockchain> {
this.messageService.log('Fetching blockchain...');
return this.http.get<Blockchain>('/api/blockchain')
.do(blockchain => {
// todo: move sorting to backend?
blockchain.chain = blockchain.chain.sort((block1: Block, block2: Block) => block2.index - block1.index);
})
.do(blockchain => this.messageService.log(`Got blockchain with ${blockchain.chain.length} block(s)`));
}
doMine(): Observable<Block> {
this.messageService.log('Mining...');
return this.http.post<Block>('/api/mine', {})
.do(block => this.messageService.log(`Successfully mined block ${block.index} with ${block.transactions.length} transaction(s)`))
;
}
}
/* DashboardComponent's private CSS styles */
[class*='col-'] {
float: left;
padding-right: 20px;
padding-bottom: 20px;
}
[class*='col-']:last-of-type {
padding-right: 0;
}
a {
text-decoration: none;
}
*, *:after, *:before {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
h3 {
text-align: center; margin-bottom: 0;
}
h4 {
position: relative;
}
.grid {
margin: 0;
}
.col-1-4 {
width: 25%;
}
.module {
padding: 20px;
text-align: center;
color: #eee;
max-height: 120px;
min-width: 120px;
background-color: #607D8B;
border-radius: 2px;
}
.module:hover {
background-color: #EEE;
cursor: pointer;
color: #607d8b;
}
.grid-pad {
padding: 10px 0;
}
.grid-pad > [class*='col-']:last-of-type {
padding-right: 20px;
}
@media (max-width: 600px) {
.module {
font-size: 10px;
max-height: 75px; }
}
@media (max-width: 1024px) {
.grid {
margin: 0;
}
.module {
min-width: 60px;
}
}
<h3>Top Transactions</h3>
<div class="grid grid-pad">
<a *ngFor="let transaction of transactions" class="col-1-4">
<div class="module hero">
<h4>{{transaction.id}}</h4>
</div>
</a>
</div>
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardComponent } from './dashboard.component';
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DashboardComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit } from '@angular/core';
import { Transaction } from '../transaction';
import { TransactionService } from '../transaction.service';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: [ './dashboard.component.css' ]
})
export class DashboardComponent implements OnInit {
transactions: Transaction[] = [];
constructor(private transactionService: TransactionService) { }
ngOnInit() {
this.getTransactions();
}
getTransactions(): void {
this.transactionService.getTransactions()
.subscribe(transactions => this.transactions = transactions.slice(1, 2));
}
}
import { InMemoryDbService } from 'angular-in-memory-web-api';
export class InMemoryDataService implements InMemoryDbService {
createDb() {
const transactions = [
{ id: '11', from: 'Mr. Nice', to: 'Michel', amount: 5 },
{ id: '20', from: 'Mr. Nice', to: 'Michel', amount: 5 }
];
return {transactions};
}
}
import { TestBed, inject } from '@angular/core/testing';
import { MessageService } from './message.service';
describe('MessageService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [MessageService]
});
});
it('should be created', inject([MessageService], (service: MessageService) => {
expect(service).toBeTruthy();
}));
});
import { Injectable } from '@angular/core';
@Injectable()
export class MessageService {
messages: string[] = [];
add(message: string) {
this.messages.push(message);
}
clear() {
this.messages = [];
}
}
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { ReplaySubject } from 'rxjs/ReplaySubject';
export interface Message {
text: string;
time: Date;
}
@Injectable()
export class MessageService {
messages$: Observable<Message[]>;
private messages: Message[] = [];