diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 7a9b53ed0..79a8e1c02 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -22,6 +22,7 @@ import { AssetsFeaturedComponent } from './components/assets/assets-featured/ass import { AssetsComponent } from './components/assets/assets.component'; import { AssetComponent } from './components/asset/asset.component'; import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component'; +import { CalculatorComponent } from './components/calculator/calculator.component'; const browserWindow = window || {}; // @ts-ignore @@ -278,6 +279,10 @@ let routes: Routes = [ path: 'rbf', component: RbfList, }, + { + path: 'tools/calculator', + component: CalculatorComponent + }, { path: 'terms-of-service', component: TermsOfServiceComponent diff --git a/frontend/src/app/components/calculator/calculator.component.html b/frontend/src/app/components/calculator/calculator.component.html new file mode 100644 index 000000000..df2146760 --- /dev/null +++ b/frontend/src/app/components/calculator/calculator.component.html @@ -0,0 +1,69 @@ +
+
+

Calculator

+
+ + + +
+ +
+
+
+ {{ currency$ | async }} +
+ + +
+ +
+
+ BTC +
+ + +
+ +
+
+ sats +
+ + +
+
+ +
+ +
+ +
+
+ ₿ + + sats +
+
+ +
+
+ +
+
+ +
+
+ Fiat price last updated +
+
+ + +
+ + +
+ Waiting for price feed... +
+
+ +
\ No newline at end of file diff --git a/frontend/src/app/components/calculator/calculator.component.scss b/frontend/src/app/components/calculator/calculator.component.scss new file mode 100644 index 000000000..5c0cbb5b1 --- /dev/null +++ b/frontend/src/app/components/calculator/calculator.component.scss @@ -0,0 +1,26 @@ +.input-group-text { + width: 75px; +} + +.bitcoin-satoshis-text { + font-size: 40px; +} + +.fiat-text { + font-size: 24px; +} + +.symbol { + font-style: italic; +} + +@media (max-width: 767.98px) { + .bitcoin-satoshis-text { + font-size: 30px; + } +} + +.sats { + font-size: 20px; + margin-left: 5px; +} \ No newline at end of file diff --git a/frontend/src/app/components/calculator/calculator.component.ts b/frontend/src/app/components/calculator/calculator.component.ts new file mode 100644 index 000000000..838afbbd4 --- /dev/null +++ b/frontend/src/app/components/calculator/calculator.component.ts @@ -0,0 +1,103 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { combineLatest, Observable } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; +import { StateService } from '../../services/state.service'; +import { WebsocketService } from '../../services/websocket.service'; + +@Component({ + selector: 'app-calculator', + templateUrl: './calculator.component.html', + styleUrls: ['./calculator.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class CalculatorComponent implements OnInit { + satoshis = 10000; + form: FormGroup; + + currency$ = this.stateService.fiatCurrency$; + price$: Observable; + lastFiatPrice$: Observable; + + constructor( + private stateService: StateService, + private formBuilder: FormBuilder, + private websocketService: WebsocketService, + ) { } + + ngOnInit(): void { + this.form = this.formBuilder.group({ + fiat: [0], + bitcoin: [0], + satoshis: [0], + }); + + this.lastFiatPrice$ = this.stateService.conversions$.asObservable() + .pipe( + map((conversions) => conversions.time) + ); + + let currency; + this.price$ = this.currency$.pipe( + switchMap((result) => { + currency = result; + return this.stateService.conversions$.asObservable(); + }), + map((conversions) => { + return conversions[currency]; + }) + ); + + combineLatest([ + this.price$, + this.form.get('fiat').valueChanges + ]).subscribe(([price, value]) => { + const rate = (value / price).toFixed(8); + const satsRate = Math.round(value / price * 100_000_000); + this.form.get('bitcoin').setValue(rate, { emitEvent: false }); + this.form.get('satoshis').setValue(satsRate, { emitEvent: false } ); + }); + + combineLatest([ + this.price$, + this.form.get('bitcoin').valueChanges + ]).subscribe(([price, value]) => { + const rate = parseFloat((value * price).toFixed(8)); + this.form.get('fiat').setValue(rate, { emitEvent: false } ); + this.form.get('satoshis').setValue(Math.round(value * 100_000_000), { emitEvent: false } ); + }); + + combineLatest([ + this.price$, + this.form.get('satoshis').valueChanges + ]).subscribe(([price, value]) => { + const rate = parseFloat((value / 100_000_000 * price).toFixed(8)); + const bitcoinRate = (value / 100_000_000).toFixed(8); + this.form.get('fiat').setValue(rate, { emitEvent: false } ); + this.form.get('bitcoin').setValue(bitcoinRate, { emitEvent: false }); + }); + + } + + transformInput(name: string): void { + const formControl = this.form.get(name); + if (!formControl.value) { + return formControl.setValue('', {emitEvent: false}); + } + let value = formControl.value.replace(',', '.').replace(/[^0-9.]/g, ''); + if (value === '.') { + value = '0'; + } + const sanitizedValue = this.removeExtraDots(value); + formControl.setValue(sanitizedValue, {emitEvent: true}); + } + + removeExtraDots(str: string): string { + const [beforeDot, afterDot] = str.split('.', 2); + if (afterDot === undefined) { + return str; + } + const afterDotReplaced = afterDot.replace(/\./g, ''); + return `${beforeDot}.${afterDotReplaced}`; + } +} diff --git a/frontend/src/app/shared/pipes/bitcoinsatoshis.pipe.ts b/frontend/src/app/shared/pipes/bitcoinsatoshis.pipe.ts new file mode 100644 index 000000000..7065b5138 --- /dev/null +++ b/frontend/src/app/shared/pipes/bitcoinsatoshis.pipe.ts @@ -0,0 +1,28 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; + +@Pipe({ + name: 'bitcoinsatoshis' +}) +export class BitcoinsatoshisPipe implements PipeTransform { + + constructor(private sanitizer: DomSanitizer) { } + + transform(value: string): SafeHtml { + const newValue = this.insertSpaces(parseFloat(value || '0').toFixed(8)); + const position = (newValue || '0').search(/[1-9]/); + + const firstPart = newValue.slice(0, position); + const secondPart = newValue.slice(position); + + return this.sanitizer.bypassSecurityTrustHtml( + `${firstPart}${secondPart}` + ); + } + + insertSpaces(str: string): string { + const length = str.length; + return str.slice(0, length - 6) + ' ' + str.slice(length - 6, length - 3) + ' ' + str.slice(length - 3); + } + +} diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 9cf780116..d56986107 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -97,6 +97,8 @@ import { MempoolBlockOverviewComponent } from '../components/mempool-block-overv import { ClockchainComponent } from '../components/clockchain/clockchain.component'; import { ClockFaceComponent } from '../components/clock-face/clock-face.component'; import { ClockComponent } from '../components/clock/clock.component'; +import { CalculatorComponent } from '../components/calculator/calculator.component'; +import { BitcoinsatoshisPipe } from '../shared/pipes/bitcoinsatoshis.pipe'; import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-directives/weight-directives'; @@ -185,12 +187,12 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir GeolocationComponent, TestnetAlertComponent, GlobalFooterComponent, - + CalculatorComponent, + BitcoinsatoshisPipe, MempoolBlockOverviewComponent, ClockchainComponent, ClockComponent, ClockFaceComponent, - OnlyVsizeDirective, OnlyWeightDirective ],