JSFiddle - React, Tailwind, and code Playground

by Josh Pullen

CSS

body {
  font-family: "CMU Serif";
  background: black;
}

.Transaction {
  position: fixed;
  top: 0;
  left: 0;
  transform: translate(-50%, -50%);
  
  font-size: 5vmin;
  color: white;
  text-decoration: none;
  white-space: nowrap;
}
.Transaction__hash {
  color: rgba(255, 255, 255, 0.4);
}
.Transaction__out {
  color: red;
}
.Transaction__in {
  color: green;
}
.Transaction__value {
  
}

.Transaction--corner0 {
  animation: transactionAnim0 6000ms ease-out forwards;
}
.Transaction--corner1 {
  animation: transactionAnim1 6000ms ease-out forwards;
}
.Transaction--corner2 {
  animation: transactionAnim2 6000ms ease-out forwards;
}
.Transaction--corner3 {
  animation: transactionAnim3 6000ms ease-out forwards;
}

@keyframes transactionAnim0 {
  0% { opacity: 1; top: -25%; left: -25%; }
  70% { opacity: 1; }
  90% { opacity: 0; pointer-events: all; }
  100% { opacity: 0; top: 40%; left: 40%; pointer-events: none; }
}
@keyframes transactionAnim1 {
  0% { opacity: 1; top: -25%; left: 125%; }
  70% { opacity: 1; }
  90% { opacity: 0; pointer-events: all; }
  100% { opacity: 0; top: 40%; left: 60%; pointer-events: none; }
}
@keyframes transactionAnim2 {
  0% { opacity: 1; top: 125%; left: 125%; }
  70% { opacity: 1; }
  90% { opacity: 0; pointer-events: all; }
  100% { opacity: 0; top: 60%; left: 60%; pointer-events: none; }
}
@keyframes transactionAnim3 {
  0% { opacity: 1; top: 125%; left: -25%; }
  70% { opacity: 1; }
  90% { opacity: 0; pointer-events: all; }
  100% { opacity: 0; top: 60%; left: 40%; pointer-events: none; }
}


.TransactionRipple {
  position: fixed;
  top: 0;
  left: 0;
  width: 0;
  height: 0;
  pointer-events: none;
}
.TransactionRipple--corner0 {
  top: 0;
  left: 0;
}
.TransactionRipple--corner1 {
  top: 0;
  left: 100%;
}
.TransactionRipple--corner2 {
  top: 100%;
  left: 100%;
}
.TransactionRipple--corner3 {
  top: 100%;
  left: 0;
}

.TransactionRipple__ripple {
  position: absolute;
  top: 0;
  left: 0;
  width: 0vmin;
  height:...

JavaScript

import { html, render, useState, useEffect } from "https://unpkg.com/htm/preact/standalone.module.js";

function getInputValue(data) {
	return data.inputs.reduce((sum, input) => input.prev_out.value + sum, 0) / 100000000;
}

function usePeriodicRerender(period = 100) {
	const [dummyValue, setDummyValue] = useState(true);
  
  useEffect(() => {
  	const rerender = () => setDummyValue(value => !value);
  	const interval = setInterval(rerender, period);
    return () => {
    	clearInterval(interval);
    }
  }, [period]);
}

function App() {
	const [transactions, setTransactions] = useState([]);
  
  const addTransaction = (data) => {
  	setTransactions(transactions => {
    	const prevTransaction = transactions.length > 0
        ? transactions[transactions.length - 1]
        : null;

      // Limit transactions to a rate of one every 200ms. Drop any
      // that come in faster. (This also prevents a bug where switching
      // to a different tab and returning later would display all the
      // transactions that happened in the background all at once.)
      if (prevTransaction && (Date.now() - prevTransaction.time) < 150) {
        return transactions;
      }
      
      // Prevent bug where switching to a different tab and returning later
      // displays lots of transactions all at once (by just skipping
      // transactions while the page isn't visible)
      if (document.visibilityState === "hidden") {
      	return transactions;
      }
      
      const prevCorner = prevTransaction ? prevTransaction.corner : 0;

      // Only keep the most recent 30 transactions in memory
      // (This should be enough so that old transactions are
      // only deleted once they have already animated out
      // of existence.)
      return [
        ...transactions.slice(-29),
        {
          data,
          corner: (prevCorner + 1) % 4,
          time: Date.now()
        }
      ];
    });
  }
  
	const { error } = useLiveBTCData({
  	onTransaction: (data)...