mirror of
https://github.com/Retropex/bitfeed.git
synced 2025-05-12 19:20:46 +02:00
387 lines
10 KiB
Svelte
387 lines
10 KiB
Svelte
<script>
|
|
import analytics from '../utils/analytics.js'
|
|
import { fly } from 'svelte/transition'
|
|
import { linear } from 'svelte/easing'
|
|
import { createEventDispatcher } from 'svelte'
|
|
import Icon from '../components/Icon.svelte'
|
|
import closeIcon from '../assets/icon/cil-x-circle.svg'
|
|
import { shortBtcFormat, longBtcFormat, dateFormat, numberFormat } from '../utils/format.js'
|
|
import { exchangeRates, settings, blocksEnabled, latestBlockHeight, blockTransitionDirection, loading } from '../stores.js'
|
|
import { formatCurrency } from '../utils/fx.js'
|
|
import { searchBlockHeight } from '../utils/search.js'
|
|
|
|
const dispatch = createEventDispatcher()
|
|
|
|
let prevBlockId
|
|
let blockId
|
|
export let block
|
|
export let visible
|
|
const newBlockDelay = 2000
|
|
let restoring = false
|
|
let formattedBlockValue = ''
|
|
|
|
$: {
|
|
if (block && block.value) {
|
|
const rate = $exchangeRates[$settings.currency]
|
|
let local
|
|
if (rate && rate.last) {
|
|
local = formatCurrency($settings.currency, (block.value/100000000) * rate.last, { compact: true })
|
|
} else {
|
|
local = null
|
|
}
|
|
formattedBlockValue = `${formatBTC(block.value)}${local != null ? (' ≈ ' + local) : ''}`
|
|
}
|
|
}
|
|
|
|
$: {
|
|
if (block && visible) {
|
|
prevBlockId = blockId
|
|
blockId = block.id
|
|
}
|
|
}
|
|
|
|
$: {
|
|
if (visible && block && block.id === prevBlockId) {
|
|
restoring = true
|
|
} else {
|
|
restoring = false
|
|
}
|
|
}
|
|
|
|
let hasPrevBlock
|
|
let hasNextBlock
|
|
$: {
|
|
if (block) {
|
|
if (block.height > 0) hasPrevBlock = true
|
|
else hasPrevBlock = false
|
|
if (block.height < $latestBlockHeight) hasNextBlock = true
|
|
else hasNextBlock = false
|
|
} else {
|
|
hasPrevBlock = false
|
|
hasNextBlock = false
|
|
}
|
|
}
|
|
|
|
let flyIn
|
|
let flyOut
|
|
$: {
|
|
if ($blockTransitionDirection && $blockTransitionDirection === 'right') {
|
|
flyIn = { x: 100, easing: linear, delay: 1000, duration: 1000 }
|
|
flyOut = { x: -100, easing: linear, delay: 0, duration: 1000 }
|
|
} else if ($blockTransitionDirection && $blockTransitionDirection === 'left') {
|
|
flyIn = { x: -100, easing: linear, delay: 1000, duration: 1000 }
|
|
flyOut = { x: 100, easing: linear, delay: 0, duration: 1000 }
|
|
} else {
|
|
flyIn = { y: (restoring ? -50 : 50), duration: (restoring ? 500 : 1000), easing: linear, delay: (restoring ? 0 : newBlockDelay) }
|
|
flyOut = { y: -50, duration: 2000, easing: linear }
|
|
}
|
|
}
|
|
|
|
function formatDateTime (time) {
|
|
return dateFormat.format(time)
|
|
}
|
|
|
|
function formatBTC (sats) {
|
|
return `₿ ${shortBtcFormat.format(sats/100000000)}`
|
|
}
|
|
|
|
function formatBytes (bytes) {
|
|
if (bytes) {
|
|
return `${numberFormat.format(bytes)} bytes`
|
|
} else return `unknown size`
|
|
}
|
|
|
|
function formatCount (n) {
|
|
if (n) {
|
|
return numberFormat.format(n)
|
|
} else return '0'
|
|
}
|
|
|
|
function formatFee (n) {
|
|
if (n) {
|
|
return numberFormat.format(n.toFixed(2))
|
|
} else return '0'
|
|
}
|
|
|
|
function hideBlock () {
|
|
if (block && block.height != $latestBlockHeight) {
|
|
dispatch('quitExploring')
|
|
} else {
|
|
analytics.trackEvent('viz', 'block', 'hide')
|
|
dispatch('hideBlock')
|
|
}
|
|
}
|
|
|
|
async function explorePrevBlock (e) {
|
|
e.preventDefault()
|
|
if (!$loading && block) {
|
|
loading.increment()
|
|
await searchBlockHeight(block.height - 1)
|
|
loading.decrement()
|
|
}
|
|
}
|
|
|
|
async function exploreNextBlock (e) {
|
|
e.preventDefault()
|
|
if (!$loading && block) {
|
|
if (block.height + 1 < $latestBlockHeight) {
|
|
loading.increment()
|
|
await searchBlockHeight(block.height + 1)
|
|
loading.decrement()
|
|
} else {
|
|
dispatch('quitExploring')
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style type="text/scss">
|
|
.close-button {
|
|
width: 1em;
|
|
height: 1em;
|
|
background: none;
|
|
border: none;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
margin: 0;
|
|
padding: 0;
|
|
cursor: pointer;
|
|
pointer-events: all;
|
|
font-size: 1.2em;
|
|
|
|
&.standalone {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
.block-info {
|
|
position: absolute;
|
|
bottom: calc(100% + 0.25rem);
|
|
left: 50%;
|
|
min-width: 100%;
|
|
transform: translateX(-50%);
|
|
pointer-events: all;
|
|
|
|
color: var(--palette-x);
|
|
font-size: 1rem;
|
|
|
|
@media (max-width: 360px) {
|
|
font-size: 4.4vw;
|
|
}
|
|
|
|
.compact {
|
|
display: none;
|
|
}
|
|
|
|
@media (max-aspect-ratio: 1/1) and (max-height: 760px) {
|
|
.compact {
|
|
display: block;
|
|
}
|
|
.full-size {
|
|
display: none;
|
|
}
|
|
}
|
|
@media (aspect-ratio: 1/1) and (max-height: 760px) {
|
|
.compact {
|
|
display: none;
|
|
}
|
|
.full-size {
|
|
display: block;
|
|
}
|
|
}
|
|
|
|
.data-row {
|
|
display: flex;
|
|
flex-direction: row;
|
|
flex-wrap: nowrap;
|
|
justify-content: space-between;
|
|
|
|
&.spacer {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
.data-field {
|
|
white-space: nowrap;
|
|
|
|
&.close-button {
|
|
width: 1em;
|
|
height: 1em;
|
|
background: none;
|
|
border: none;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
margin: 0;
|
|
padding: 0;
|
|
cursor: pointer;
|
|
margin-top: -5px;
|
|
}
|
|
|
|
&:first-child {
|
|
margin-right: 5px;
|
|
}
|
|
&:last-child {
|
|
margin-left: 5px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.explore-button {
|
|
position: absolute;
|
|
bottom: 10%;
|
|
padding: .75em;
|
|
pointer-events: all;
|
|
|
|
&.prev {
|
|
right: 100%
|
|
}
|
|
&.next {
|
|
left: 100%;
|
|
}
|
|
|
|
.chevron {
|
|
.outline {
|
|
stroke: white;
|
|
stroke-width: 32;
|
|
stroke-linecap: butt;
|
|
stroke-linejoin: miter;
|
|
fill: white;
|
|
fill-opacity: 0;
|
|
transition: fill-opacity 300ms;
|
|
}
|
|
|
|
&.right {
|
|
transform: scaleX(-1);
|
|
}
|
|
}
|
|
|
|
&:hover {
|
|
.chevron .outline {
|
|
fill-opacity: 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
@media (min-aspect-ratio: 1/1) {
|
|
.block-info {
|
|
bottom: unset;
|
|
left: unset;
|
|
top: 0;
|
|
right: 100%;
|
|
padding-right: .5rem;
|
|
|
|
min-width: 0;
|
|
transform: translateX(0);
|
|
|
|
.data-row {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: flex-start;
|
|
align-items: flex-end;
|
|
|
|
&.spacer {
|
|
display: flex;
|
|
}
|
|
}
|
|
|
|
.data-field {
|
|
white-space: wrap;
|
|
margin-left: 0;
|
|
margin-right: 5px;
|
|
|
|
&.title-field {
|
|
margin-bottom: .5em;
|
|
}
|
|
|
|
&.close-button {
|
|
display: none;
|
|
}
|
|
}
|
|
}
|
|
|
|
.standalone.close-button {
|
|
display: block;
|
|
position: absolute;
|
|
bottom: 100%;
|
|
left: 100%;
|
|
margin: 5px;
|
|
}
|
|
}
|
|
|
|
@media (min-aspect-ratio: 1/1) and (max-height: 400px) {
|
|
.standalone.close-button {
|
|
top: 0;
|
|
bottom: unset;
|
|
margin-top: 0;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
{#key block}
|
|
{#if block != null && visible && $blocksEnabled }
|
|
<div class="block-info" out:fly={flyOut} in:fly={flyIn}>
|
|
<!-- <span class="data-field">Hash: { block.id }</span> -->
|
|
<div class="full-size">
|
|
<div class="data-row">
|
|
<span class="data-field title-field" title="{block.miner_sig}"><b>{#if block.height == $latestBlockHeight}Latest {/if}Block: </b>{ numberFormat.format(block.height) }</span>
|
|
<button class="data-field close-button" on:click={hideBlock}><Icon icon={closeIcon} color="var(--palette-x)" /></button>
|
|
</div>
|
|
<div class="data-row">
|
|
<span class="data-field" title="block timestamp">{ formatDateTime(block.time) }</span>
|
|
<span class="data-field">{ formattedBlockValue }</span>
|
|
</div>
|
|
<div class="data-row">
|
|
<span class="data-field">{ formatBytes(block.bytes) }</span>
|
|
<span class="data-field">{ formatCount(block.txnCount) } transactions</span>
|
|
</div>
|
|
<div class="data-row spacer"> </div>
|
|
<div class="data-row">
|
|
<span class="data-field">Avg fee rate</span>
|
|
{#if block.fees != null}
|
|
<span class="data-field">{ formatFee(block.avgFeerate) } sats/vbyte</span>
|
|
{:else}
|
|
<span class="data-field">unavailable</span>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
<div class="compact">
|
|
<div class="data-row">
|
|
<span class="data-field title-field" title="{block.miner_sig}"><b>Latest Block: </b>{ numberFormat.format(block.height) }</span>
|
|
<button class="data-field close-button" on:click={hideBlock}><Icon icon={closeIcon} color="var(--palette-x)" /></button>
|
|
</div>
|
|
<div class="data-row">
|
|
<span class="data-field">{ formatDateTime(block.time) }</span>
|
|
<span class="data-field">{ formattedBlockValue }</span>
|
|
</div>
|
|
<div class="data-row">
|
|
<span class="data-field">{ formatCount(block.txnCount) } transactions</span>
|
|
{#if block.fees != null}
|
|
<span class="data-field">{ formatFee(block.avgFeerate) } sats/vb</span>
|
|
{:else}
|
|
<span class="data-field">{ formatBytes(block.bytes) }</span>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{#if hasPrevBlock }
|
|
<a href="/block/height/{block.height - 1}" on:click={explorePrevBlock} class="explore-button prev" out:fly={flyOut} in:fly={flyIn}>
|
|
<svg class="chevron left" height="1.5em" width="1.5em" viewBox="0 0 512 512">
|
|
<path d="M 107.628,257.54 327.095,38.078 404,114.989 261.506,257.483 404,399.978 327.086,476.89 Z" class="outline" />
|
|
</svg>
|
|
</a>
|
|
{/if}
|
|
{#if hasNextBlock }
|
|
<a href="/block/height/{block.height + 1}" on:click={exploreNextBlock} class="explore-button next" out:fly={flyOut} in:fly={flyIn}>
|
|
<svg class="chevron right" height="1.5em" width="1.5em" viewBox="0 0 512 512">
|
|
<path d="M 107.628,257.54 327.095,38.078 404,114.989 261.506,257.483 404,399.978 327.086,476.89 Z" class="outline" />
|
|
</svg>
|
|
</a>
|
|
{/if}
|
|
<button class="close-button standalone" on:click={hideBlock} out:fly={flyOut} in:fly={flyIn} >
|
|
<Icon icon={closeIcon} color="var(--palette-x)" />
|
|
</button>
|
|
{/if}
|
|
{/key}
|