Skip to content

Commit

Permalink
Feat/web UI (#4)
Browse files Browse the repository at this point in the history
* Add UI and mock DB

* Add stats cache

* Add stats to ui

* Refactor be structure

* Improve ui

* Fix test endpoint

* Update readme

* Fix integration tests

* Decrease polling interval
  • Loading branch information
heppu committed Mar 3, 2024
1 parent 6186d70 commit 3f0cb0d
Show file tree
Hide file tree
Showing 21 changed files with 995 additions and 79 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
# devnet-explorer
Gevulot Devnet Explorer

## Running

### With Postgres
To use devnet-explorer against real database pass postgres DNS via DNS env variable.
Run `mage go:run` and open UI at [http://127.0.0.1:8383](http://127.0.0.1:8383).


### With mock data
Devnet explorer can be executed without DB using mock data.
Run `mage go:runWithMockDB` and open UI at [http://127.0.0.1:8383](http://127.0.0.1:8383).

## Development

### Requirements
Expand Down
24 changes: 17 additions & 7 deletions api/api.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
package api

import (
"embed"
"encoding/json"
"fmt"
"io/fs"
"log/slog"
"net/http"

"github.com/gevulotnetwork/devnet-explorer/model"
"github.com/julienschmidt/httprouter"
)

//go:embed all:public
var public embed.FS

type Store interface {
Stats() (model.Stats, error)
}
Expand All @@ -18,23 +24,27 @@ type API struct {
s Store
}

func New(s Store) *API {
func New(s Store) (*API, error) {
a := &API{
r: httprouter.New(),
s: s,
}
a.bind()
return a

publicFS, err := fs.Sub(public, "public")
if err != nil {
return nil, fmt.Errorf("failed to create public fs: %w", err)
}

a.r.NotFound = http.FileServer(http.FS(publicFS))
a.r.GET("/api/v1/stats", a.stats)

return a, nil
}

func (a *API) ServeHTTP(w http.ResponseWriter, r *http.Request) {
a.r.ServeHTTP(w, r)
}

func (a *API) bind() {
a.r.GET("/api/v1/stat", a.stats)
}

func (a *API) stats(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
stats, err := a.s.Stats()
if err != nil {
Expand Down
Binary file added api/public/assets/SpaceGrotesk-Bold.ttf
Binary file not shown.
Binary file added api/public/assets/SpaceGrotesk-Light.ttf
Binary file not shown.
Binary file added api/public/assets/SpaceGrotesk-Regular.ttf
Binary file not shown.
Binary file added api/public/assets/SpaceMono-Regular.ttf
Binary file not shown.
175 changes: 175 additions & 0 deletions api/public/assets/gevulot-rain.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
(function initRAF() {
const vendors = ["webkit", "moz"];
for (const vendor of vendors) {
if (window.requestAnimationFrame) break;
window.requestAnimationFrame = window[`${vendor}RequestAnimationFrame`];
window.cancelAnimationFrame =
window[`${vendor}CancelAnimationFrame`] ||
window[`${vendor}CancelRequestAnimationFrame`];
}

if (!window.requestAnimationFrame) {
let lastTime = 0;
window.requestAnimationFrame = function (callback) {
const currTime = new Date().getTime();
const timeToCall = Math.max(0, 16 - (currTime - lastTime));
const id = setTimeout(() => callback(currTime + timeToCall), timeToCall);
lastTime = currTime + timeToCall;
return id;
};
}

if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame = (id) => clearTimeout(id);
}
})();

class Matrix {
constructor(canvas) {
this.canvas = canvas;
this.updateDimensions();
this.ctx = canvas.getContext("2d");

this.ctx.font = "30px Courier New";
this.xSpacing = 10;
this.ySpacing = 10;
this.speed = 0.2;
this.devicePixelRatio = window.devicePixelRatio || 1;

this.yPositions = Array(Math.ceil(this.width / this.xSpacing))
.fill(0)
.map(() => Math.random() * (this.height / this.ySpacing));

this.directions = Array(Math.ceil(this.width / this.xSpacing))
.fill(0)
.map(() => (Math.random() < 0.5 ? 1 : 1)); // 1 for down, -1 for up

this.ySpeeds = this.yPositions.map(
() => (Math.random() + 0.2) * this.speed
);
this.yTimes = this.yPositions.map(() => 0);
this.lastChars = this.yPositions.map(() => " ");
window.addEventListener("mousemove", (e) => this.onMouseMove(e));
}

onMouseMove(event) {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;

const col = Math.floor(x / this.xSpacing);
this.yPositions[col] = y / this.ySpacing;
this.yTimes[col] = 0; // Reset the time for this column
}

updateDimensions() {
const dpr = window.devicePixelRatio || 1;
this.devicePixelRatio = dpr; // Store the dpr
this.width = window.innerWidth;
this.height = window.innerHeight;
this.canvas.width = this.width * dpr;
this.canvas.height = this.height * dpr;
this.canvas.style.width = `${this.width}px`;
this.canvas.style.height = `${this.height}px`;
}

draw() {
requestAnimationFrame(this.draw.bind(this));

// Drawing logic here
this.ctx.fillStyle = "rgba(0, 0, 0, 0.05)";
this.ctx.fillRect(0, 0, this.width, this.height);
this.ctx.fillStyle = "#A678ED";

const charArr = [
"⫖",
"⫈",
"⪸",
"⪬",
"⫛",
"⫏",
"⫐",
"⩱",
"⩸",
"⩦",
"⩨",
"⩢",
"⩽",
"⨺",
"⨻",
"⩥",
"⩩",
"⫒",
"⫕",
"⪫",
"⪭",
"⫑",
"⫓",
"⪷",
"⪵",
];

this.yPositions.forEach((y, i) => {
if (this.yTimes[i] > 1) {
const char = charArr[Math.floor(Math.random() * charArr.length)];
this.lastChars[i] = char;

this.ctx.fillText(
char,
i * this.xSpacing + 1,
y * this.ySpacing + this.ySpacing
);

this.yPositions[i] =
y + this.directions[i] < 0
? this.height / this.ySpacing
: y + this.directions[i] >= this.height / this.ySpacing
? 0
: y + this.directions[i];

this.yTimes[i] = 0;
}
this.yTimes[i] += this.ySpeeds[i];
});
}

start() {
this.draw();
}

resize() {
this.updateDimensions();
this.ctx.setTransform(
this.devicePixelRatio,
0,
0,
this.devicePixelRatio,
0,
0
);

// You might also want to update the directions array here, if needed

const columns = Math.ceil(this.width / this.xSpacing);
while (this.yPositions.length < columns) {
this.yPositions.push(Math.random() * (this.height / this.ySpacing));
this.ySpeeds.push((Math.random() + 0.2) * this.speed);
this.yTimes.push(0);
this.lastChars.push(" ");
this.directions.push(Math.random() < 0.5 ? 1 : 1);
}

if (this.yPositions.length > columns) {
this.yPositions = this.yPositions.slice(0, columns);
this.ySpeeds = this.ySpeeds.slice(0, columns);
this.yTimes = this.yTimes.slice(0, columns);
this.lastChars = this.lastChars.slice(0, columns);
this.directions = this.directions.slice(0, columns);
}
}
}

const matrix = new Matrix(document.getElementById("matrixCanvas"));
window.addEventListener("resize", () => matrix.resize());
matrix.resize(); // Initialize dimensions
matrix.start();
97 changes: 97 additions & 0 deletions api/public/assets/numbers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
let ticks;
let fresh = true;
const time = 1000;
const minDigits = 6

function scrollNumber(counter, digits) {
counter.querySelectorAll('span[data-value]').forEach((tick, i) => {
tick.style.transform = `translateY(-${100 * parseInt(digits[i])}%)`;
})

counter.style.width = `${digits.length * 5.1}rem`;
}

function addDigit(counter, digit, fresh) {
const spanList = Array(10)
.join(0)
.split(0)
.map((x, j) => `<span>${j}</span>`)
.join('')

counter.insertAdjacentHTML(
"beforeend",
`<span style="transform: translateY(-1000%)" data-value="${digit}">
${spanList}
</span>`)

const firstDigit = counter.lastElementChild

setTimeout(() => {
firstDigit.className = "visible";
}, fresh ? 0 : 2000);
}

function removeDigit(counter) {
const firstDigit = counter.lastElementChild
firstDigit.classList.remove("visible");
setTimeout(() => {
firstDigit.remove();
}, 2000);
}

function setup(counter) {
console.log(counter)
startNum = 0
const digits = startNum.toString().split('')

for (let i = 0; i < minDigits; i++) {
addDigit(counter, '0', true)
}

scrollNumber(counter, ['0'])

setTimeout(() => scrollNumber(counter, digits), 2000)

counter.dataset.value = startNum;
}

function rollToNumber(idx, num) {
el.style.transform = `translateY(-${100 - num * 10}%)`;
}

function update(counter, num) {
const toDigits = num.toString().split('')
const fromDigits = counter.dataset.value.toString().split('')
console.log(fromDigits, toDigits)

for (let i = fromDigits.length - toDigits.length; i > 0; i--) {
removeDigit(counter)
}
for (let i = toDigits.length - fromDigits.length; i > 0; i--) {
addDigit(counter, toDigits[i]);
}

scrollNumber(counter, toDigits)
counter.dataset.value = num
}

function fetchData() {
fetch('/api/v1/stats')
.then(res => res.json())
.then(data => {
for (let key in data) {
console.log(key, data[key])
update(document.getElementById(key), data[key])
}
})
.catch(err => console.error(err))
}

function setupCounters() {
for (const element of document.getElementsByClassName('rolling-number')) {
setup(element);
}
}

setupCounters();
setInterval(fetchData, 5000)
Loading

0 comments on commit 3f0cb0d

Please sign in to comment.