mirror of
https://github.com/Retropex/custom-ocean.xyz-dashboard.git
synced 2025-05-12 19:20:45 +02:00
Add files via upload
This commit is contained in:
parent
6fcbfa806d
commit
8c5c39d435
852
static/js/BitcoinProgressBar.js
Normal file
852
static/js/BitcoinProgressBar.js
Normal file
@ -0,0 +1,852 @@
|
||||
/**
|
||||
* BitcoinMinuteRefresh.js - A minute-based refresh system tied to server uptime
|
||||
*
|
||||
* This module creates a Bitcoin-themed terminal that shows server uptime
|
||||
* and refreshes data only on minute boundaries for better synchronization.
|
||||
*/
|
||||
|
||||
const BitcoinMinuteRefresh = (function () {
|
||||
// Constants
|
||||
const STORAGE_KEY = 'bitcoin_last_refresh_time'; // For cross-page sync
|
||||
|
||||
// Private variables
|
||||
let terminalElement = null;
|
||||
let uptimeElement = null;
|
||||
let serverTimeOffset = 0;
|
||||
let serverStartTime = null;
|
||||
let uptimeInterval = null;
|
||||
let lastMinuteValue = -1;
|
||||
let isInitialized = false;
|
||||
let refreshCallback = null;
|
||||
|
||||
/**
|
||||
* Create and inject the retro terminal element into the DOM
|
||||
*/
|
||||
function createTerminalElement() {
|
||||
// Container element
|
||||
terminalElement = document.createElement('div');
|
||||
terminalElement.id = 'bitcoin-terminal';
|
||||
terminalElement.className = 'bitcoin-terminal';
|
||||
|
||||
// Terminal content
|
||||
terminalElement.innerHTML = `
|
||||
<div class="terminal-header">
|
||||
<div class="terminal-title">SYSTEM MONITOR v.3</div>
|
||||
<div class="terminal-controls">
|
||||
<div class="terminal-dot minimize" title="Minimize" onclick="BitcoinMinuteRefresh.toggleTerminal()"></div>
|
||||
<div class="terminal-dot close" title="Close" onclick="BitcoinMinuteRefresh.hideTerminal()"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="terminal-content">
|
||||
<div class="status-row">
|
||||
<div class="status-indicator">
|
||||
<div class="status-dot connected"></div>
|
||||
<span>LIVE</span>
|
||||
</div>
|
||||
<span id="terminal-clock" class="terminal-clock">00:00:00</span>
|
||||
</div>
|
||||
|
||||
<div class="minute-progress-container">
|
||||
<div class="minute-labels">
|
||||
<span>0s</span>
|
||||
<span>15s</span>
|
||||
<span>30s</span>
|
||||
<span>45s</span>
|
||||
<span>60s</span>
|
||||
</div>
|
||||
<div class="minute-progress-bar">
|
||||
<div id="minute-progress-inner" class="minute-progress-inner">
|
||||
<div class="minute-progress-fill"></div>
|
||||
<div class="scan-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="refresh-status" class="refresh-status">
|
||||
Next refresh at the top of the minute
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="uptime-timer" class="uptime-timer">
|
||||
<div class="uptime-title">UPTIME</div>
|
||||
<div class="uptime-display">
|
||||
<div class="uptime-value">
|
||||
<span id="uptime-hours" class="uptime-number">00</span>
|
||||
<span class="uptime-label">H</span>
|
||||
</div>
|
||||
<div class="uptime-separator">:</div>
|
||||
<div class="uptime-value">
|
||||
<span id="uptime-minutes" class="uptime-number">00</span>
|
||||
<span class="uptime-label">M</span>
|
||||
</div>
|
||||
<div class="uptime-separator">:</div>
|
||||
<div class="uptime-value">
|
||||
<span id="uptime-seconds" class="uptime-number">00</span>
|
||||
<span class="uptime-label">S</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="terminal-minimized">
|
||||
<!-- <div class="minimized-clock">
|
||||
<span id="minimized-clock-value">00:00:00</span>
|
||||
</div>
|
||||
<div class="minimized-separator">|</div> -->
|
||||
<div class="minimized-uptime">
|
||||
<span class="mini-uptime-label">UPTIME</span>
|
||||
<span id="minimized-uptime-value">00:00:00</span>
|
||||
</div>
|
||||
<div class="minimized-status-dot connected"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Append to body
|
||||
document.body.appendChild(terminalElement);
|
||||
|
||||
// Cache element references
|
||||
uptimeElement = document.getElementById('uptime-timer');
|
||||
|
||||
// Check if terminal was previously collapsed
|
||||
if (localStorage.getItem('bitcoin_terminal_collapsed') === 'true') {
|
||||
terminalElement.classList.add('collapsed');
|
||||
}
|
||||
|
||||
// Add custom styles if not already present
|
||||
if (!document.getElementById('bitcoin-terminal-styles')) {
|
||||
addStyles();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add CSS styles for the terminal
|
||||
*/
|
||||
function addStyles() {
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.id = 'bitcoin-terminal-styles';
|
||||
styleElement.textContent = `
|
||||
/* Terminal Container */
|
||||
.bitcoin-terminal {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 340px;
|
||||
background-color: #000000;
|
||||
border: 1px solid #f7931a; // Changed from 2px to 1px
|
||||
color: #f7931a;
|
||||
font-family: 'VT323', monospace;
|
||||
z-index: 9999;
|
||||
overflow: hidden;
|
||||
padding: 8px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 0 5px rgba(247, 147, 26, 0.3); // Added to match card shadow
|
||||
}
|
||||
|
||||
/* Terminal Header */
|
||||
.terminal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #f7931a;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.terminal-title {
|
||||
color: #f7931a;
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
text-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
|
||||
animation: terminal-flicker 4s infinite;
|
||||
}
|
||||
|
||||
/* Control Dots */
|
||||
.terminal-controls {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.terminal-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #555;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.terminal-dot.minimize:hover {
|
||||
background-color: #ffcc00;
|
||||
}
|
||||
|
||||
.terminal-dot.close:hover {
|
||||
background-color: #ff3b30;
|
||||
}
|
||||
|
||||
/* Terminal Content */
|
||||
.terminal-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Status Row */
|
||||
.status-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Status Indicator */
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.status-dot.connected {
|
||||
background-color: #32CD32;
|
||||
box-shadow: 0 0 5px #32CD32;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.terminal-clock {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 5px rgba(247, 147, 26, 0.5);
|
||||
}
|
||||
|
||||
/* Minute Progress Bar */
|
||||
.minute-progress-container {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.minute-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.7rem;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.minute-progress-bar {
|
||||
width: 100%;
|
||||
height: 15px;
|
||||
background-color: #111;
|
||||
border: 1px solid #f7931a;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.minute-progress-inner {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.minute-progress-fill {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: linear-gradient(90deg, #f7931a, #ffa500);
|
||||
transition: width 0.5s ease-out;
|
||||
}
|
||||
|
||||
/* Scan line effect */
|
||||
.scan-line {
|
||||
position: absolute;
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
animation: scan 2s linear infinite;
|
||||
box-shadow: 0 0 8px 1px rgba(255, 255, 255, 0.5);
|
||||
z-index: 2;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* Refresh status */
|
||||
.refresh-status {
|
||||
font-size: 0.8rem;
|
||||
margin-top: 5px;
|
||||
text-align: center;
|
||||
text-shadow: 0 0 5px rgba(247, 147, 26, 0.5);
|
||||
}
|
||||
|
||||
/* Uptime Display - Modern Digital Clock Style (Horizontal) */
|
||||
.uptime-timer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
background-color: #111;
|
||||
border: 1px solid rgba(247, 147, 26, 0.5);
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.uptime-display {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.uptime-value {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.uptime-number {
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
background-color: #000;
|
||||
padding: 2px 5px;
|
||||
border-radius: 3px;
|
||||
min-width: 32px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
letter-spacing: 2px;
|
||||
text-shadow: 0 0 8px rgba(247, 147, 26, 0.8);
|
||||
color: #f7931a;
|
||||
}
|
||||
|
||||
.uptime-label {
|
||||
font-size: 0.7rem;
|
||||
opacity: 0.7;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.uptime-separator {
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
padding: 0 2px;
|
||||
text-shadow: 0 0 8px rgba(247, 147, 26, 0.8);
|
||||
}
|
||||
|
||||
.uptime-title {
|
||||
font-size: 0.7rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
text-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
/* Special effects */
|
||||
.minute-progress-inner.near-refresh .minute-progress-fill {
|
||||
box-shadow: 0 0 15px #f7931a, 0 0 25px #f7931a;
|
||||
animation: pulse-brightness 1s infinite;
|
||||
}
|
||||
|
||||
.minute-progress-inner.refresh-now .minute-progress-fill {
|
||||
animation: refresh-flash 1s forwards;
|
||||
}
|
||||
|
||||
/* Show button */
|
||||
#bitcoin-terminal-show {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
background-color: #f7931a;
|
||||
color: #000;
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
font-family: 'VT323', monospace;
|
||||
cursor: pointer;
|
||||
z-index: 9999;
|
||||
display: none;
|
||||
box-shadow: 0 0 10px rgba(247, 147, 26, 0.5);
|
||||
}
|
||||
|
||||
/* CRT scanline effect */
|
||||
.terminal-content::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
rgba(0, 0, 0, 0.15),
|
||||
rgba(0, 0, 0, 0.15) 1px,
|
||||
transparent 1px,
|
||||
transparent 2px
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Minimized view styling */
|
||||
.terminal-minimized {
|
||||
display: none;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 10px;
|
||||
background-color: #000;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.terminal-minimized::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
rgba(0, 0, 0, 0.15),
|
||||
rgba(0, 0, 0, 0.15) 1px,
|
||||
transparent 1px,
|
||||
transparent 2px
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.minimized-clock {
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 5px rgba(247, 147, 26, 0.5);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.minimized-separator {
|
||||
margin: 0 10px;
|
||||
color: rgba(247, 147, 26, 0.5);
|
||||
font-size: 1.1rem;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.minimized-uptime {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.mini-uptime-label {
|
||||
font-size: 0.6rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
opacity: 0.7;
|
||||
margin-left: 45px;
|
||||
color: #f7931a;
|
||||
}
|
||||
|
||||
#minimized-uptime-value {
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 5px rgba(247, 147, 26, 0.5);
|
||||
margin-left: 45px;
|
||||
color: #f7931a;
|
||||
}
|
||||
|
||||
.minimized-status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
margin-left: 10px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Collapsed state */
|
||||
.bitcoin-terminal.collapsed {
|
||||
width: auto;
|
||||
max-width: 500px;
|
||||
height: auto;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.bitcoin-terminal.collapsed .terminal-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bitcoin-terminal.collapsed .terminal-minimized {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.bitcoin-terminal.collapsed .terminal-header {
|
||||
border-bottom: none;
|
||||
margin-bottom: 2px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes scan {
|
||||
0% { top: -2px; }
|
||||
100% { top: 17px; }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 0.8; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes terminal-flicker {
|
||||
0% { opacity: 0.97; }
|
||||
5% { opacity: 0.95; }
|
||||
10% { opacity: 0.97; }
|
||||
15% { opacity: 0.94; }
|
||||
20% { opacity: 0.98; }
|
||||
50% { opacity: 0.95; }
|
||||
80% { opacity: 0.96; }
|
||||
90% { opacity: 0.94; }
|
||||
100% { opacity: 0.98; }
|
||||
}
|
||||
|
||||
@keyframes pulse-brightness {
|
||||
0%, 100% { filter: brightness(1); }
|
||||
50% { filter: brightness(1.3); }
|
||||
}
|
||||
|
||||
@keyframes refresh-flash {
|
||||
0% { filter: brightness(1); background-color: #f7931a; }
|
||||
10% { filter: brightness(1.8); background-color: #fff; }
|
||||
20% { filter: brightness(1); background-color: #f7931a; }
|
||||
30% { filter: brightness(1.8); background-color: #fff; }
|
||||
40% { filter: brightness(1); background-color: #f7931a; }
|
||||
100% { filter: brightness(1); background-color: #f7931a; }
|
||||
}
|
||||
|
||||
/* Media Queries */
|
||||
@media (max-width: 768px) {
|
||||
.bitcoin-terminal {
|
||||
left: 50%;
|
||||
right: auto;
|
||||
transform: translateX(-50%);
|
||||
width: 90%;
|
||||
max-width: 320px;
|
||||
bottom: 10px;
|
||||
}
|
||||
|
||||
.bitcoin-terminal.collapsed {
|
||||
width: auto;
|
||||
max-width: 300px;
|
||||
left: 50%; // Changed from "left: auto"
|
||||
right: auto; // Changed from "right: 10px"
|
||||
transform: translateX(-50%); // Changed from "transform: none"
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
document.head.appendChild(styleElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the minute progress bar based on current seconds
|
||||
*/
|
||||
function updateMinuteProgress() {
|
||||
try {
|
||||
// Get current server time
|
||||
const now = new Date(Date.now() + (serverTimeOffset || 0));
|
||||
const seconds = now.getSeconds();
|
||||
const milliseconds = now.getMilliseconds();
|
||||
|
||||
// Calculate precise progress within the minute (0-1)
|
||||
const progress = (seconds + (milliseconds / 1000)) / 60;
|
||||
|
||||
// Update the progress bar
|
||||
const progressBar = document.getElementById('minute-progress-inner');
|
||||
if (progressBar) {
|
||||
// Set the width based on the current progress through the minute
|
||||
progressBar.style.width = (progress * 100) + "%";
|
||||
|
||||
// Add effects when close to the end of the minute
|
||||
if (seconds >= 50) {
|
||||
progressBar.classList.add('near-refresh');
|
||||
document.getElementById('refresh-status').textContent = `Refresh in ${60 - seconds} seconds...`;
|
||||
} else {
|
||||
progressBar.classList.remove('near-refresh');
|
||||
document.getElementById('refresh-status').textContent = 'Next refresh at the top of the minute';
|
||||
}
|
||||
|
||||
// Flash when reaching 00 seconds (new minute)
|
||||
if (seconds === 0 && milliseconds < 500) {
|
||||
progressBar.classList.add('refresh-now');
|
||||
document.getElementById('refresh-status').textContent = 'Refreshing data...';
|
||||
|
||||
// Remove the class after animation completes
|
||||
setTimeout(() => {
|
||||
progressBar.classList.remove('refresh-now');
|
||||
}, 1000);
|
||||
} else {
|
||||
progressBar.classList.remove('refresh-now');
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we've crossed into a new minute
|
||||
const currentMinute = now.getMinutes();
|
||||
if (lastMinuteValue !== -1 && currentMinute !== lastMinuteValue && seconds === 0) {
|
||||
// Trigger refresh on minute change (only when seconds are 0)
|
||||
if (typeof refreshCallback === 'function') {
|
||||
console.log('New minute started - triggering refresh...');
|
||||
refreshCallback();
|
||||
}
|
||||
}
|
||||
|
||||
// Update last minute value
|
||||
lastMinuteValue = currentMinute;
|
||||
|
||||
} catch (e) {
|
||||
console.error("BitcoinMinuteRefresh: Error updating progress bar:", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the terminal clock
|
||||
*/
|
||||
function updateClock() {
|
||||
try {
|
||||
const now = new Date(Date.now() + (serverTimeOffset || 0));
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||
const timeString = `${hours}:${minutes}:${seconds}`;
|
||||
|
||||
// Update both clocks (normal and minimized views)
|
||||
const clockElement = document.getElementById('terminal-clock');
|
||||
if (clockElement) {
|
||||
clockElement.textContent = timeString;
|
||||
}
|
||||
|
||||
const minimizedClockElement = document.getElementById('minimized-clock-value');
|
||||
if (minimizedClockElement) {
|
||||
minimizedClockElement.textContent = timeString;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("BitcoinMinuteRefresh: Error updating clock:", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the uptime display
|
||||
*/
|
||||
function updateUptime() {
|
||||
if (serverStartTime) {
|
||||
try {
|
||||
const currentServerTime = Date.now() + serverTimeOffset;
|
||||
const diff = currentServerTime - serverStartTime;
|
||||
|
||||
// Calculate hours, minutes, seconds
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
|
||||
|
||||
// Update the main uptime display with digital clock style
|
||||
document.getElementById('uptime-hours').textContent = String(hours).padStart(2, '0');
|
||||
document.getElementById('uptime-minutes').textContent = String(minutes).padStart(2, '0');
|
||||
document.getElementById('uptime-seconds').textContent = String(seconds).padStart(2, '0');
|
||||
|
||||
// Update the minimized uptime display
|
||||
const minimizedUptimeElement = document.getElementById('minimized-uptime-value');
|
||||
if (minimizedUptimeElement) {
|
||||
minimizedUptimeElement.textContent = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("BitcoinMinuteRefresh: Error updating uptime:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify other tabs that data has been refreshed
|
||||
*/
|
||||
function notifyRefresh() {
|
||||
const now = Date.now();
|
||||
localStorage.setItem(STORAGE_KEY, now.toString());
|
||||
localStorage.setItem('bitcoin_refresh_event', 'refresh-' + now);
|
||||
console.log("BitcoinMinuteRefresh: Notified other tabs of refresh at " + new Date(now).toISOString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the minute refresh system
|
||||
*/
|
||||
function initialize(refreshFunc) {
|
||||
// Store the refresh callback
|
||||
refreshCallback = refreshFunc;
|
||||
|
||||
// Create the terminal element if it doesn't exist
|
||||
if (!document.getElementById('bitcoin-terminal')) {
|
||||
createTerminalElement();
|
||||
} else {
|
||||
// Get references to existing elements
|
||||
terminalElement = document.getElementById('bitcoin-terminal');
|
||||
uptimeElement = document.getElementById('uptime-timer');
|
||||
}
|
||||
|
||||
// Try to get stored server time information
|
||||
try {
|
||||
serverTimeOffset = parseFloat(localStorage.getItem('serverTimeOffset') || '0');
|
||||
serverStartTime = parseFloat(localStorage.getItem('serverStartTime') || '0');
|
||||
} catch (e) {
|
||||
console.error("BitcoinMinuteRefresh: Error reading server time from localStorage:", e);
|
||||
}
|
||||
|
||||
// Clear any existing intervals
|
||||
if (uptimeInterval) {
|
||||
clearInterval(uptimeInterval);
|
||||
}
|
||||
|
||||
// Set up intervals for updating at 50ms precision for smooth animation
|
||||
uptimeInterval = setInterval(function () {
|
||||
updateClock();
|
||||
updateUptime();
|
||||
updateMinuteProgress();
|
||||
}, 50);
|
||||
|
||||
// Listen for storage events to sync across tabs
|
||||
window.removeEventListener('storage', handleStorageChange);
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
|
||||
// Handle visibility changes
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
// Initialize last minute value
|
||||
const now = new Date(Date.now() + serverTimeOffset);
|
||||
lastMinuteValue = now.getMinutes();
|
||||
|
||||
// Log current server time details for debugging
|
||||
console.log(`BitcoinMinuteRefresh: Server time is ${now.toISOString()}, minute=${lastMinuteValue}, seconds=${now.getSeconds()}`);
|
||||
|
||||
// Mark as initialized
|
||||
isInitialized = true;
|
||||
|
||||
console.log("BitcoinMinuteRefresh: Initialized");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle storage changes for cross-tab synchronization
|
||||
*/
|
||||
function handleStorageChange(event) {
|
||||
if (event.key === 'bitcoin_refresh_event') {
|
||||
console.log("BitcoinMinuteRefresh: Detected refresh from another tab");
|
||||
|
||||
// If another tab refreshed, consider refreshing this one too
|
||||
// But don't refresh if it was just refreshed recently (5 seconds)
|
||||
const lastRefreshTime = parseInt(localStorage.getItem(STORAGE_KEY) || '0');
|
||||
if (typeof refreshCallback === 'function' && Date.now() - lastRefreshTime > 5000) {
|
||||
refreshCallback();
|
||||
}
|
||||
} else if (event.key === 'serverTimeOffset' || event.key === 'serverStartTime') {
|
||||
try {
|
||||
serverTimeOffset = parseFloat(localStorage.getItem('serverTimeOffset') || '0');
|
||||
serverStartTime = parseFloat(localStorage.getItem('serverStartTime') || '0');
|
||||
} catch (e) {
|
||||
console.error("BitcoinMinuteRefresh: Error reading updated server time:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle visibility changes
|
||||
*/
|
||||
function handleVisibilityChange() {
|
||||
if (!document.hidden) {
|
||||
console.log("BitcoinMinuteRefresh: Page became visible, updating");
|
||||
|
||||
// Update immediately when page becomes visible
|
||||
updateClock();
|
||||
updateUptime();
|
||||
updateMinuteProgress();
|
||||
|
||||
// Check if we need to do a refresh based on time elapsed
|
||||
if (typeof refreshCallback === 'function') {
|
||||
const lastRefreshTime = parseInt(localStorage.getItem(STORAGE_KEY) || '0');
|
||||
if (Date.now() - lastRefreshTime > 60000) { // More than a minute since last refresh
|
||||
refreshCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update server time information
|
||||
*/
|
||||
function updateServerTime(timeOffset, startTime) {
|
||||
serverTimeOffset = timeOffset;
|
||||
serverStartTime = startTime;
|
||||
|
||||
// Store in localStorage for cross-page sharing
|
||||
localStorage.setItem('serverTimeOffset', serverTimeOffset.toString());
|
||||
localStorage.setItem('serverStartTime', serverStartTime.toString());
|
||||
|
||||
// Update the uptime immediately
|
||||
updateUptime();
|
||||
updateMinuteProgress();
|
||||
|
||||
console.log("BitcoinMinuteRefresh: Server time updated - offset:", serverTimeOffset, "ms");
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle terminal collapsed state
|
||||
*/
|
||||
function toggleTerminal() {
|
||||
if (!terminalElement) return;
|
||||
|
||||
terminalElement.classList.toggle('collapsed');
|
||||
localStorage.setItem('bitcoin_terminal_collapsed', terminalElement.classList.contains('collapsed'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the terminal and show the restore button
|
||||
*/
|
||||
function hideTerminal() {
|
||||
if (!terminalElement) return;
|
||||
|
||||
terminalElement.style.display = 'none';
|
||||
|
||||
// Create show button if it doesn't exist
|
||||
if (!document.getElementById('bitcoin-terminal-show')) {
|
||||
const showButton = document.createElement('button');
|
||||
showButton.id = 'bitcoin-terminal-show';
|
||||
showButton.textContent = 'Show Monitor';
|
||||
showButton.onclick = showTerminal;
|
||||
document.body.appendChild(showButton);
|
||||
}
|
||||
|
||||
document.getElementById('bitcoin-terminal-show').style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the terminal and hide the restore button
|
||||
*/
|
||||
function showTerminal() {
|
||||
if (!terminalElement) return;
|
||||
|
||||
terminalElement.style.display = 'block';
|
||||
document.getElementById('bitcoin-terminal-show').style.display = 'none';
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
initialize: initialize,
|
||||
notifyRefresh: notifyRefresh,
|
||||
updateServerTime: updateServerTime,
|
||||
toggleTerminal: toggleTerminal,
|
||||
hideTerminal: hideTerminal,
|
||||
showTerminal: showTerminal
|
||||
};
|
||||
})();
|
||||
|
||||
// Auto-initialize when document is ready if a refresh function is available in the global scope
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Check if manualRefresh function exists in global scope
|
||||
if (typeof window.manualRefresh === 'function') {
|
||||
BitcoinMinuteRefresh.initialize(window.manualRefresh);
|
||||
} else {
|
||||
console.log("BitcoinMinuteRefresh: No refresh function found, will need to be initialized manually");
|
||||
}
|
||||
});
|
384
static/js/block-animation.js
Normal file
384
static/js/block-animation.js
Normal file
@ -0,0 +1,384 @@
|
||||
// Bitcoin Block Mining Animation Controller
|
||||
class BlockMiningAnimation {
|
||||
constructor(svgContainerId) {
|
||||
// Get the container element
|
||||
this.container = document.getElementById(svgContainerId);
|
||||
if (!this.container) {
|
||||
console.error("SVG container not found:", svgContainerId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get SVG elements
|
||||
this.blockHeight = document.getElementById("block-height");
|
||||
this.statusHeight = document.getElementById("status-height");
|
||||
this.miningPool = document.getElementById("mining-pool");
|
||||
this.blockTime = document.getElementById("block-time");
|
||||
this.transactionCount = document.getElementById("transaction-count");
|
||||
this.miningHash = document.getElementById("mining-hash");
|
||||
this.nonceValue = document.getElementById("nonce-value");
|
||||
this.difficultyValue = document.getElementById("difficulty-value");
|
||||
this.miningStatus = document.getElementById("mining-status");
|
||||
|
||||
// Debug element availability
|
||||
console.log("Animation elements found:", {
|
||||
blockHeight: !!this.blockHeight,
|
||||
statusHeight: !!this.statusHeight,
|
||||
miningPool: !!this.miningPool,
|
||||
blockTime: !!this.blockTime,
|
||||
transactionCount: !!this.transactionCount,
|
||||
miningHash: !!this.miningHash,
|
||||
nonceValue: !!this.nonceValue,
|
||||
difficultyValue: !!this.difficultyValue,
|
||||
miningStatus: !!this.miningStatus
|
||||
});
|
||||
|
||||
// Animation state
|
||||
this.animationPhase = "collecting"; // collecting, mining, found, adding
|
||||
this.miningSpeed = 300; // ms between nonce updates
|
||||
this.nonceCounter = 0;
|
||||
this.currentBlockData = null;
|
||||
this.animationInterval = null;
|
||||
this.apiRetryCount = 0;
|
||||
this.maxApiRetries = 3;
|
||||
|
||||
// Initialize random hash for mining animation
|
||||
this.updateRandomHash();
|
||||
}
|
||||
|
||||
// Start the animation loop
|
||||
start() {
|
||||
if (this.animationInterval) {
|
||||
clearInterval(this.animationInterval);
|
||||
}
|
||||
|
||||
console.log("Starting block mining animation");
|
||||
this.animationInterval = setInterval(() => this.animationTick(), this.miningSpeed);
|
||||
|
||||
// Start by fetching the latest block
|
||||
this.fetchLatestBlockWithRetry();
|
||||
}
|
||||
|
||||
// Stop the animation
|
||||
stop() {
|
||||
if (this.animationInterval) {
|
||||
clearInterval(this.animationInterval);
|
||||
this.animationInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Main animation tick function
|
||||
animationTick() {
|
||||
switch (this.animationPhase) {
|
||||
case "collecting":
|
||||
// Simulate collecting transactions
|
||||
this.updateTransactionAnimation();
|
||||
break;
|
||||
|
||||
case "mining":
|
||||
// Update nonce and hash values
|
||||
this.updateMiningAnimation();
|
||||
break;
|
||||
|
||||
case "found":
|
||||
// Block found phase - brief celebration
|
||||
this.updateFoundAnimation();
|
||||
break;
|
||||
|
||||
case "adding":
|
||||
// Adding block to chain
|
||||
this.updateAddingAnimation();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch latest block with retry logic
|
||||
fetchLatestBlockWithRetry() {
|
||||
this.apiRetryCount = 0;
|
||||
this.fetchLatestBlock();
|
||||
}
|
||||
|
||||
// Fetch the latest block data from mempool.space
|
||||
fetchLatestBlock() {
|
||||
console.log("Fetching latest block data, attempt #" + (this.apiRetryCount + 1));
|
||||
|
||||
// Show that we're fetching
|
||||
if (this.miningStatus) {
|
||||
this.miningStatus.textContent = "Connecting to blockchain...";
|
||||
}
|
||||
|
||||
// Use the mempool.space public API
|
||||
fetch("https://mempool.space/api/v1/blocks/tip/height")
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch latest block height: " + response.status);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(height => {
|
||||
console.log("Latest block height:", height);
|
||||
// Fetch multiple blocks but limit to 1
|
||||
return fetch(`https://mempool.space/api/v1/blocks?height=${height}&limit=1`);
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch block data: " + response.status);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(blockData => {
|
||||
console.log("Block data received:", blockData);
|
||||
|
||||
// Ensure we have data and use the first block
|
||||
if (blockData && blockData.length > 0) {
|
||||
this.currentBlockData = blockData[0];
|
||||
this.startBlockAnimation();
|
||||
|
||||
// Reset retry count on success
|
||||
this.apiRetryCount = 0;
|
||||
} else {
|
||||
throw new Error("No block data received");
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error fetching block data:", error);
|
||||
|
||||
// Retry logic
|
||||
this.apiRetryCount++;
|
||||
if (this.apiRetryCount < this.maxApiRetries) {
|
||||
console.log(`Retrying in 2 seconds... (attempt ${this.apiRetryCount + 1}/${this.maxApiRetries})`);
|
||||
setTimeout(() => this.fetchLatestBlock(), 2000);
|
||||
} else {
|
||||
console.warn("Max retries reached, using placeholder data");
|
||||
// Use placeholder data if fetch fails after retries
|
||||
this.usePlaceholderData();
|
||||
this.startBlockAnimation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Start the block animation sequence
|
||||
startBlockAnimation() {
|
||||
// Reset animation state
|
||||
this.animationPhase = "collecting";
|
||||
this.nonceCounter = 0;
|
||||
|
||||
// Update block data display immediately
|
||||
this.updateBlockDisplay();
|
||||
|
||||
// Schedule the animation sequence
|
||||
setTimeout(() => {
|
||||
this.animationPhase = "mining";
|
||||
if (this.miningStatus) {
|
||||
this.miningStatus.textContent = "Mining in progress...";
|
||||
}
|
||||
|
||||
// After a random mining period, find the block
|
||||
setTimeout(() => {
|
||||
this.animationPhase = "found";
|
||||
if (this.miningStatus) {
|
||||
this.miningStatus.textContent = "BLOCK FOUND!";
|
||||
}
|
||||
|
||||
// Then move to adding phase
|
||||
setTimeout(() => {
|
||||
this.animationPhase = "adding";
|
||||
if (this.miningStatus) {
|
||||
this.miningStatus.textContent = "Adding to blockchain...";
|
||||
}
|
||||
|
||||
// After adding, fetch a new block or loop with current one
|
||||
setTimeout(() => {
|
||||
// Fetch a new block every time to keep data current
|
||||
this.fetchLatestBlockWithRetry();
|
||||
}, 3000);
|
||||
}, 2000);
|
||||
}, 5000 + Math.random() * 5000); // Random mining time
|
||||
}, 3000); // Time for collecting transactions
|
||||
}
|
||||
|
||||
// Update block display with current block data
|
||||
updateBlockDisplay() {
|
||||
if (!this.currentBlockData) {
|
||||
console.error("No block data available to display");
|
||||
return;
|
||||
}
|
||||
|
||||
// Safely extract and format block data
|
||||
const blockData = Array.isArray(this.currentBlockData)
|
||||
? this.currentBlockData[0]
|
||||
: this.currentBlockData;
|
||||
|
||||
console.log("Updating block display with data:", blockData);
|
||||
|
||||
try {
|
||||
// Safely extract and format block height
|
||||
const height = blockData.height ? blockData.height.toString() : "N/A";
|
||||
if (this.blockHeight) this.blockHeight.textContent = height;
|
||||
if (this.statusHeight) this.statusHeight.textContent = height;
|
||||
|
||||
// Safely format block timestamp
|
||||
let formattedTime = "N/A";
|
||||
if (blockData.timestamp) {
|
||||
const timestamp = new Date(blockData.timestamp * 1000);
|
||||
formattedTime = timestamp.toLocaleString();
|
||||
}
|
||||
if (this.blockTime) this.blockTime.textContent = formattedTime;
|
||||
|
||||
// Safely format transaction count
|
||||
const txCount = blockData.tx_count ? blockData.tx_count.toString() : "N/A";
|
||||
if (this.transactionCount) this.transactionCount.textContent = txCount;
|
||||
|
||||
// Format mining pool
|
||||
let poolName = "Unknown";
|
||||
if (blockData.extras && blockData.extras.pool && blockData.extras.pool.name) {
|
||||
poolName = blockData.extras.pool.name;
|
||||
}
|
||||
if (this.miningPool) this.miningPool.textContent = poolName;
|
||||
|
||||
// Format difficulty (simplified)
|
||||
let difficultyStr = "Unknown";
|
||||
if (blockData.difficulty) {
|
||||
// Format as scientific notation for better display
|
||||
difficultyStr = blockData.difficulty.toExponential(2);
|
||||
}
|
||||
if (this.difficultyValue) this.difficultyValue.textContent = difficultyStr;
|
||||
|
||||
// Use actual nonce if available
|
||||
if (this.nonceValue && blockData.nonce) {
|
||||
this.nonceValue.textContent = blockData.nonce.toString();
|
||||
// Use this as starting point for animation
|
||||
this.nonceCounter = blockData.nonce;
|
||||
}
|
||||
|
||||
// Update block hash (if available)
|
||||
if (this.miningHash && blockData.id) {
|
||||
const blockHash = blockData.id;
|
||||
const shortHash = blockHash.substring(0, 8) + "..." + blockHash.substring(blockHash.length - 8);
|
||||
this.miningHash.textContent = shortHash;
|
||||
}
|
||||
|
||||
console.log("Block display updated successfully");
|
||||
} catch (error) {
|
||||
console.error("Error updating block display:", error, "Block data:", blockData);
|
||||
}
|
||||
}
|
||||
|
||||
// Transaction collection animation
|
||||
updateTransactionAnimation() {
|
||||
// Animation for collecting transactions is handled by SVG animation
|
||||
// We could add additional logic here if needed
|
||||
}
|
||||
|
||||
// Mining animation - update nonce and hash
|
||||
updateMiningAnimation() {
|
||||
// Increment nonce
|
||||
this.nonceCounter += 1 + Math.floor(Math.random() * 1000);
|
||||
if (this.nonceValue) {
|
||||
this.nonceValue.textContent = this.nonceCounter.toString().padStart(10, '0');
|
||||
}
|
||||
|
||||
// Update hash value
|
||||
this.updateRandomHash();
|
||||
}
|
||||
|
||||
// Block found animation - show a hash that matches difficulty
|
||||
updateFoundAnimation() {
|
||||
if (!this.miningHash || !this.nonceValue || !this.currentBlockData) return;
|
||||
|
||||
try {
|
||||
// Make the "found" hash start with enough zeros based on difficulty
|
||||
// Use actual block hash if available
|
||||
const blockData = Array.isArray(this.currentBlockData)
|
||||
? this.currentBlockData[0]
|
||||
: this.currentBlockData;
|
||||
|
||||
if (blockData.id) {
|
||||
const blockHash = blockData.id;
|
||||
const shortHash = blockHash.substring(0, 8) + "..." + blockHash.substring(blockHash.length - 8);
|
||||
this.miningHash.textContent = shortHash;
|
||||
} else {
|
||||
// Fallback to generated hash
|
||||
const zeros = Math.min(6, Math.max(2, Math.floor(Math.log10(blockData.difficulty) / 10)));
|
||||
const zeroPrefix = '0'.repeat(zeros);
|
||||
const remainingChars = '0123456789abcdef';
|
||||
let hash = zeroPrefix;
|
||||
|
||||
// Fill the rest with random hex characters
|
||||
for (let i = zeros; i < 8; i++) {
|
||||
hash += remainingChars.charAt(Math.floor(Math.random() * remainingChars.length));
|
||||
}
|
||||
|
||||
this.miningHash.textContent = hash + "..." + hash;
|
||||
}
|
||||
|
||||
// Use the actual nonce if available
|
||||
if (blockData.nonce) {
|
||||
this.nonceValue.textContent = blockData.nonce.toString();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating found animation:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Adding block to chain animation
|
||||
updateAddingAnimation() {
|
||||
// Animation for adding to blockchain is handled by SVG animation
|
||||
// We could add additional logic here if needed
|
||||
}
|
||||
|
||||
// Generate a random hash string for mining animation
|
||||
updateRandomHash() {
|
||||
if (!this.miningHash) return;
|
||||
|
||||
const characters = '0123456789abcdef';
|
||||
let hash = '';
|
||||
|
||||
// Generate random 8-char segment
|
||||
for (let i = 0; i < 8; i++) {
|
||||
hash += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
|
||||
this.miningHash.textContent = hash + "..." + hash;
|
||||
}
|
||||
|
||||
// Use placeholder data if API fetch fails
|
||||
usePlaceholderData() {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
this.currentBlockData = {
|
||||
height: 888888,
|
||||
timestamp: now,
|
||||
tx_count: 2500,
|
||||
difficulty: 50000000000000,
|
||||
nonce: 123456789,
|
||||
id: "00000000000000000000b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7",
|
||||
extras: {
|
||||
pool: {
|
||||
name: "Placeholder Pool"
|
||||
}
|
||||
}
|
||||
};
|
||||
console.log("Using placeholder data:", this.currentBlockData);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize and start the animation when the page loads
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
console.log("DOM content loaded, initializing animation");
|
||||
|
||||
// Ensure we give the SVG enough time to be fully rendered and accessible
|
||||
setTimeout(() => {
|
||||
const svgContainer = document.getElementById("svg-container");
|
||||
if (!svgContainer) {
|
||||
console.error("SVG container not found in DOM");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const animation = new BlockMiningAnimation("svg-container");
|
||||
animation.start();
|
||||
console.log("Animation started successfully");
|
||||
} catch (error) {
|
||||
console.error("Error starting animation:", error);
|
||||
}
|
||||
}, 1500); // Increased delay to ensure SVG is fully loaded
|
||||
});
|
762
static/js/blocks.js
Normal file
762
static/js/blocks.js
Normal file
@ -0,0 +1,762 @@
|
||||
"use strict";
|
||||
|
||||
// Global variables
|
||||
let currentStartHeight = null;
|
||||
const mempoolBaseUrl = "https://mempool.space";
|
||||
let blocksCache = {};
|
||||
let isLoading = false;
|
||||
|
||||
// DOM ready initialization
|
||||
$(document).ready(function() {
|
||||
console.log("Blocks page initialized");
|
||||
|
||||
// Load the latest blocks on page load
|
||||
loadLatestBlocks();
|
||||
|
||||
// Set up event listeners
|
||||
$("#load-blocks").on("click", function() {
|
||||
const height = $("#block-height").val();
|
||||
if (height && !isNaN(height)) {
|
||||
loadBlocksFromHeight(height);
|
||||
} else {
|
||||
showToast("Please enter a valid block height");
|
||||
}
|
||||
});
|
||||
|
||||
$("#latest-blocks").on("click", loadLatestBlocks);
|
||||
|
||||
// Handle Enter key on the block height input
|
||||
$("#block-height").on("keypress", function(e) {
|
||||
if (e.which === 13) {
|
||||
const height = $(this).val();
|
||||
if (height && !isNaN(height)) {
|
||||
loadBlocksFromHeight(height);
|
||||
} else {
|
||||
showToast("Please enter a valid block height");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Close the modal when clicking the X or outside the modal
|
||||
$(".block-modal-close").on("click", closeModal);
|
||||
$(window).on("click", function(event) {
|
||||
if ($(event.target).hasClass("block-modal")) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize BitcoinMinuteRefresh if available
|
||||
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.initialize) {
|
||||
BitcoinMinuteRefresh.initialize(loadLatestBlocks);
|
||||
console.log("BitcoinMinuteRefresh initialized with refresh function");
|
||||
}
|
||||
});
|
||||
|
||||
// Helper function to format timestamps as readable dates
|
||||
function formatTimestamp(timestamp) {
|
||||
const date = new Date(timestamp * 1000);
|
||||
return date.toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to format numbers with commas
|
||||
function numberWithCommas(x) {
|
||||
if (x == null) return "N/A";
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
// Helper function to format file sizes
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes < 1024) return bytes + " B";
|
||||
else if (bytes < 1048576) return (bytes / 1024).toFixed(2) + " KB";
|
||||
else return (bytes / 1048576).toFixed(2) + " MB";
|
||||
}
|
||||
|
||||
// Helper function to show toast messages
|
||||
function showToast(message) {
|
||||
// Check if we already have a toast container
|
||||
let toastContainer = $(".toast-container");
|
||||
if (toastContainer.length === 0) {
|
||||
// Create a new toast container
|
||||
toastContainer = $("<div>", {
|
||||
class: "toast-container",
|
||||
css: {
|
||||
position: "fixed",
|
||||
bottom: "20px",
|
||||
right: "20px",
|
||||
zIndex: 9999
|
||||
}
|
||||
}).appendTo("body");
|
||||
}
|
||||
|
||||
// Create a new toast
|
||||
const toast = $("<div>", {
|
||||
class: "toast",
|
||||
text: message,
|
||||
css: {
|
||||
backgroundColor: "#f7931a",
|
||||
color: "#000",
|
||||
padding: "10px 15px",
|
||||
borderRadius: "5px",
|
||||
marginTop: "10px",
|
||||
boxShadow: "0 0 10px rgba(247, 147, 26, 0.5)",
|
||||
fontFamily: "var(--terminal-font)",
|
||||
opacity: 0,
|
||||
transition: "opacity 0.3s ease"
|
||||
}
|
||||
}).appendTo(toastContainer);
|
||||
|
||||
// Show the toast
|
||||
setTimeout(() => {
|
||||
toast.css("opacity", 1);
|
||||
|
||||
// Hide and remove the toast after 3 seconds
|
||||
setTimeout(() => {
|
||||
toast.css("opacity", 0);
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Function to load blocks from a specific height
|
||||
function loadBlocksFromHeight(height) {
|
||||
if (isLoading) return;
|
||||
|
||||
// Convert to integer
|
||||
height = parseInt(height);
|
||||
if (isNaN(height) || height < 0) {
|
||||
showToast("Please enter a valid block height");
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
currentStartHeight = height;
|
||||
|
||||
// Check if we already have this data in cache
|
||||
if (blocksCache[height]) {
|
||||
displayBlocks(blocksCache[height]);
|
||||
isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
$("#blocks-grid").html('<div class="loader"><span class="loader-text">Loading blocks from height ' + height + '<span class="terminal-cursor"></span></span></div>');
|
||||
|
||||
// Fetch blocks from the API
|
||||
$.ajax({
|
||||
url: `${mempoolBaseUrl}/api/v1/blocks/${height}`,
|
||||
method: "GET",
|
||||
dataType: "json",
|
||||
timeout: 10000,
|
||||
success: function(data) {
|
||||
// Cache the data
|
||||
blocksCache[height] = data;
|
||||
|
||||
// Display the blocks
|
||||
displayBlocks(data);
|
||||
|
||||
// Update latest block stats
|
||||
if (data.length > 0) {
|
||||
updateLatestBlockStats(data[0]);
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error("Error fetching blocks:", error);
|
||||
$("#blocks-grid").html('<div class="error">Error fetching blocks. Please try again later.</div>');
|
||||
|
||||
// Show error toast
|
||||
showToast("Failed to load blocks. Please try again later.");
|
||||
},
|
||||
complete: function() {
|
||||
isLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Function to load the latest blocks
|
||||
function loadLatestBlocks() {
|
||||
if (isLoading) return;
|
||||
|
||||
isLoading = true;
|
||||
|
||||
// Show loading state
|
||||
$("#blocks-grid").html('<div class="loader"><span class="loader-text">Loading latest blocks<span class="terminal-cursor"></span></span></div>');
|
||||
|
||||
// Fetch the latest blocks from the API
|
||||
$.ajax({
|
||||
url: `${mempoolBaseUrl}/api/v1/blocks`,
|
||||
method: "GET",
|
||||
dataType: "json",
|
||||
timeout: 10000,
|
||||
success: function(data) {
|
||||
// Cache the data (use the first block's height as the key)
|
||||
if (data.length > 0) {
|
||||
currentStartHeight = data[0].height;
|
||||
blocksCache[currentStartHeight] = data;
|
||||
|
||||
// Update the block height input with the latest height
|
||||
$("#block-height").val(currentStartHeight);
|
||||
|
||||
// Update latest block stats
|
||||
updateLatestBlockStats(data[0]);
|
||||
}
|
||||
|
||||
// Display the blocks
|
||||
displayBlocks(data);
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error("Error fetching latest blocks:", error);
|
||||
$("#blocks-grid").html('<div class="error">Error fetching blocks. Please try again later.</div>');
|
||||
|
||||
// Show error toast
|
||||
showToast("Failed to load latest blocks. Please try again later.");
|
||||
},
|
||||
complete: function() {
|
||||
isLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Function to update the latest block stats section
|
||||
function updateLatestBlockStats(block) {
|
||||
if (!block) return;
|
||||
|
||||
$("#latest-height").text(block.height);
|
||||
$("#latest-time").text(formatTimestamp(block.timestamp));
|
||||
$("#latest-tx-count").text(numberWithCommas(block.tx_count));
|
||||
$("#latest-size").text(formatFileSize(block.size));
|
||||
$("#latest-difficulty").text(numberWithCommas(Math.round(block.difficulty)));
|
||||
|
||||
// Pool info
|
||||
if (block.extras && block.extras.pool) {
|
||||
$("#latest-pool").text(block.extras.pool.name);
|
||||
} else {
|
||||
$("#latest-pool").text("Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
// Function to display the blocks in the grid
|
||||
function displayBlocks(blocks) {
|
||||
const blocksGrid = $("#blocks-grid");
|
||||
|
||||
// Clear the grid
|
||||
blocksGrid.empty();
|
||||
|
||||
if (!blocks || blocks.length === 0) {
|
||||
blocksGrid.html('<div class="no-blocks">No blocks found</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a card for each block
|
||||
blocks.forEach(function(block) {
|
||||
const blockCard = createBlockCard(block);
|
||||
blocksGrid.append(blockCard);
|
||||
});
|
||||
|
||||
// Add navigation controls if needed
|
||||
addNavigationControls(blocks);
|
||||
}
|
||||
|
||||
// Function to create a block card
|
||||
function createBlockCard(block) {
|
||||
const timestamp = formatTimestamp(block.timestamp);
|
||||
const formattedSize = formatFileSize(block.size);
|
||||
const formattedTxCount = numberWithCommas(block.tx_count);
|
||||
|
||||
// Get the pool name or "Unknown"
|
||||
const poolName = block.extras && block.extras.pool ? block.extras.pool.name : "Unknown";
|
||||
|
||||
// Calculate total fees in BTC
|
||||
const totalFees = block.extras ? (block.extras.totalFees / 100000000).toFixed(8) : "N/A";
|
||||
|
||||
// Create the block card
|
||||
const blockCard = $("<div>", {
|
||||
class: "block-card",
|
||||
"data-height": block.height,
|
||||
"data-hash": block.id
|
||||
});
|
||||
|
||||
// Create the block header
|
||||
const blockHeader = $("<div>", {
|
||||
class: "block-header"
|
||||
});
|
||||
|
||||
blockHeader.append($("<div>", {
|
||||
class: "block-height",
|
||||
text: "#" + block.height
|
||||
}));
|
||||
|
||||
blockHeader.append($("<div>", {
|
||||
class: "block-time",
|
||||
text: timestamp
|
||||
}));
|
||||
|
||||
blockCard.append(blockHeader);
|
||||
|
||||
// Create the block info section
|
||||
const blockInfo = $("<div>", {
|
||||
class: "block-info"
|
||||
});
|
||||
|
||||
// Add transaction count
|
||||
const txCountItem = $("<div>", {
|
||||
class: "block-info-item"
|
||||
});
|
||||
txCountItem.append($("<div>", {
|
||||
class: "block-info-label",
|
||||
text: "Transactions"
|
||||
}));
|
||||
txCountItem.append($("<div>", {
|
||||
class: "block-info-value white",
|
||||
text: formattedTxCount
|
||||
}));
|
||||
blockInfo.append(txCountItem);
|
||||
|
||||
// Add size
|
||||
const sizeItem = $("<div>", {
|
||||
class: "block-info-item"
|
||||
});
|
||||
sizeItem.append($("<div>", {
|
||||
class: "block-info-label",
|
||||
text: "Size"
|
||||
}));
|
||||
sizeItem.append($("<div>", {
|
||||
class: "block-info-value white",
|
||||
text: formattedSize
|
||||
}));
|
||||
blockInfo.append(sizeItem);
|
||||
|
||||
// Add miner/pool
|
||||
const minerItem = $("<div>", {
|
||||
class: "block-info-item"
|
||||
});
|
||||
minerItem.append($("<div>", {
|
||||
class: "block-info-label",
|
||||
text: "Miner"
|
||||
}));
|
||||
minerItem.append($("<div>", {
|
||||
class: "block-info-value green",
|
||||
text: poolName
|
||||
}));
|
||||
blockInfo.append(minerItem);
|
||||
|
||||
// Add total fees
|
||||
const feesItem = $("<div>", {
|
||||
class: "block-info-item"
|
||||
});
|
||||
feesItem.append($("<div>", {
|
||||
class: "block-info-label",
|
||||
text: "Total Fees"
|
||||
}));
|
||||
feesItem.append($("<div>", {
|
||||
class: "block-info-value yellow",
|
||||
text: totalFees + " BTC"
|
||||
}));
|
||||
blockInfo.append(feesItem);
|
||||
|
||||
blockCard.append(blockInfo);
|
||||
|
||||
// Add event listener for clicking on the block card
|
||||
blockCard.on("click", function() {
|
||||
showBlockDetails(block);
|
||||
});
|
||||
|
||||
return blockCard;
|
||||
}
|
||||
|
||||
// Function to add navigation controls to the blocks grid
|
||||
function addNavigationControls(blocks) {
|
||||
// Get the height of the first and last block in the current view
|
||||
const firstBlockHeight = blocks[0].height;
|
||||
const lastBlockHeight = blocks[blocks.length - 1].height;
|
||||
|
||||
// Create navigation controls
|
||||
const navControls = $("<div>", {
|
||||
class: "block-navigation"
|
||||
});
|
||||
|
||||
// Newer blocks button (if not already at the latest blocks)
|
||||
if (firstBlockHeight !== currentStartHeight) {
|
||||
const newerButton = $("<button>", {
|
||||
class: "block-button",
|
||||
text: "Newer Blocks"
|
||||
});
|
||||
|
||||
newerButton.on("click", function() {
|
||||
loadBlocksFromHeight(firstBlockHeight + 15);
|
||||
});
|
||||
|
||||
navControls.append(newerButton);
|
||||
}
|
||||
|
||||
// Older blocks button
|
||||
const olderButton = $("<button>", {
|
||||
class: "block-button",
|
||||
text: "Older Blocks"
|
||||
});
|
||||
|
||||
olderButton.on("click", function() {
|
||||
loadBlocksFromHeight(lastBlockHeight - 1);
|
||||
});
|
||||
|
||||
navControls.append(olderButton);
|
||||
|
||||
// Add the navigation controls to the blocks grid
|
||||
$("#blocks-grid").append(navControls);
|
||||
}
|
||||
|
||||
// Function to show block details in a modal
|
||||
function showBlockDetails(block) {
|
||||
const modal = $("#block-modal");
|
||||
const blockDetails = $("#block-details");
|
||||
|
||||
// Clear the details
|
||||
blockDetails.empty();
|
||||
|
||||
// Format the timestamp
|
||||
const timestamp = formatTimestamp(block.timestamp);
|
||||
|
||||
// Create the block header section
|
||||
const headerSection = $("<div>", {
|
||||
class: "block-detail-section"
|
||||
});
|
||||
|
||||
headerSection.append($("<div>", {
|
||||
class: "block-detail-title",
|
||||
text: "Block #" + block.height
|
||||
}));
|
||||
|
||||
// Add block hash
|
||||
const hashItem = $("<div>", {
|
||||
class: "block-detail-item"
|
||||
});
|
||||
hashItem.append($("<div>", {
|
||||
class: "block-detail-label",
|
||||
text: "Block Hash"
|
||||
}));
|
||||
hashItem.append($("<div>", {
|
||||
class: "block-hash",
|
||||
text: block.id
|
||||
}));
|
||||
headerSection.append(hashItem);
|
||||
|
||||
// Add timestamp
|
||||
const timeItem = $("<div>", {
|
||||
class: "block-detail-item"
|
||||
});
|
||||
timeItem.append($("<div>", {
|
||||
class: "block-detail-label",
|
||||
text: "Timestamp"
|
||||
}));
|
||||
timeItem.append($("<div>", {
|
||||
class: "block-detail-value",
|
||||
text: timestamp
|
||||
}));
|
||||
headerSection.append(timeItem);
|
||||
|
||||
// Add merkle root
|
||||
const merkleItem = $("<div>", {
|
||||
class: "block-detail-item"
|
||||
});
|
||||
merkleItem.append($("<div>", {
|
||||
class: "block-detail-label",
|
||||
text: "Merkle Root"
|
||||
}));
|
||||
merkleItem.append($("<div>", {
|
||||
class: "block-hash",
|
||||
text: block.merkle_root
|
||||
}));
|
||||
headerSection.append(merkleItem);
|
||||
|
||||
// Add previous block hash
|
||||
const prevHashItem = $("<div>", {
|
||||
class: "block-detail-item"
|
||||
});
|
||||
prevHashItem.append($("<div>", {
|
||||
class: "block-detail-label",
|
||||
text: "Previous Block"
|
||||
}));
|
||||
prevHashItem.append($("<div>", {
|
||||
class: "block-hash",
|
||||
text: block.previousblockhash
|
||||
}));
|
||||
headerSection.append(prevHashItem);
|
||||
|
||||
blockDetails.append(headerSection);
|
||||
|
||||
// Create the mining section
|
||||
const miningSection = $("<div>", {
|
||||
class: "block-detail-section"
|
||||
});
|
||||
|
||||
miningSection.append($("<div>", {
|
||||
class: "block-detail-title",
|
||||
text: "Mining Details"
|
||||
}));
|
||||
|
||||
// Add miner/pool
|
||||
const minerItem = $("<div>", {
|
||||
class: "block-detail-item"
|
||||
});
|
||||
minerItem.append($("<div>", {
|
||||
class: "block-detail-label",
|
||||
text: "Miner"
|
||||
}));
|
||||
const poolName = block.extras && block.extras.pool ? block.extras.pool.name : "Unknown";
|
||||
minerItem.append($("<div>", {
|
||||
class: "block-detail-value",
|
||||
text: poolName
|
||||
}));
|
||||
miningSection.append(minerItem);
|
||||
|
||||
// Add difficulty
|
||||
const difficultyItem = $("<div>", {
|
||||
class: "block-detail-item"
|
||||
});
|
||||
difficultyItem.append($("<div>", {
|
||||
class: "block-detail-label",
|
||||
text: "Difficulty"
|
||||
}));
|
||||
difficultyItem.append($("<div>", {
|
||||
class: "block-detail-value",
|
||||
text: numberWithCommas(Math.round(block.difficulty))
|
||||
}));
|
||||
miningSection.append(difficultyItem);
|
||||
|
||||
// Add nonce
|
||||
const nonceItem = $("<div>", {
|
||||
class: "block-detail-item"
|
||||
});
|
||||
nonceItem.append($("<div>", {
|
||||
class: "block-detail-label",
|
||||
text: "Nonce"
|
||||
}));
|
||||
nonceItem.append($("<div>", {
|
||||
class: "block-detail-value",
|
||||
text: numberWithCommas(block.nonce)
|
||||
}));
|
||||
miningSection.append(nonceItem);
|
||||
|
||||
// Add bits
|
||||
const bitsItem = $("<div>", {
|
||||
class: "block-detail-item"
|
||||
});
|
||||
bitsItem.append($("<div>", {
|
||||
class: "block-detail-label",
|
||||
text: "Bits"
|
||||
}));
|
||||
bitsItem.append($("<div>", {
|
||||
class: "block-detail-value",
|
||||
text: block.bits
|
||||
}));
|
||||
miningSection.append(bitsItem);
|
||||
|
||||
// Add version
|
||||
const versionItem = $("<div>", {
|
||||
class: "block-detail-item"
|
||||
});
|
||||
versionItem.append($("<div>", {
|
||||
class: "block-detail-label",
|
||||
text: "Version"
|
||||
}));
|
||||
versionItem.append($("<div>", {
|
||||
class: "block-detail-value",
|
||||
text: "0x" + block.version.toString(16)
|
||||
}));
|
||||
miningSection.append(versionItem);
|
||||
|
||||
blockDetails.append(miningSection);
|
||||
|
||||
// Create the transaction section
|
||||
const txSection = $("<div>", {
|
||||
class: "block-detail-section"
|
||||
});
|
||||
|
||||
txSection.append($("<div>", {
|
||||
class: "block-detail-title",
|
||||
text: "Transaction Details"
|
||||
}));
|
||||
|
||||
// Add transaction count
|
||||
const txCountItem = $("<div>", {
|
||||
class: "block-detail-item"
|
||||
});
|
||||
txCountItem.append($("<div>", {
|
||||
class: "block-detail-label",
|
||||
text: "Transaction Count"
|
||||
}));
|
||||
txCountItem.append($("<div>", {
|
||||
class: "block-detail-value",
|
||||
text: numberWithCommas(block.tx_count)
|
||||
}));
|
||||
txSection.append(txCountItem);
|
||||
|
||||
// Add size
|
||||
const sizeItem = $("<div>", {
|
||||
class: "block-detail-item"
|
||||
});
|
||||
sizeItem.append($("<div>", {
|
||||
class: "block-detail-label",
|
||||
text: "Size"
|
||||
}));
|
||||
sizeItem.append($("<div>", {
|
||||
class: "block-detail-value",
|
||||
text: formatFileSize(block.size)
|
||||
}));
|
||||
txSection.append(sizeItem);
|
||||
|
||||
// Add weight
|
||||
const weightItem = $("<div>", {
|
||||
class: "block-detail-item"
|
||||
});
|
||||
weightItem.append($("<div>", {
|
||||
class: "block-detail-label",
|
||||
text: "Weight"
|
||||
}));
|
||||
weightItem.append($("<div>", {
|
||||
class: "block-detail-value",
|
||||
text: numberWithCommas(block.weight) + " WU"
|
||||
}));
|
||||
txSection.append(weightItem);
|
||||
|
||||
blockDetails.append(txSection);
|
||||
|
||||
// Create the fee section if available
|
||||
if (block.extras) {
|
||||
const feeSection = $("<div>", {
|
||||
class: "block-detail-section"
|
||||
});
|
||||
|
||||
feeSection.append($("<div>", {
|
||||
class: "block-detail-title",
|
||||
text: "Fee Details"
|
||||
}));
|
||||
|
||||
// Add total fees
|
||||
const totalFeesItem = $("<div>", {
|
||||
class: "block-detail-item"
|
||||
});
|
||||
totalFeesItem.append($("<div>", {
|
||||
class: "block-detail-label",
|
||||
text: "Total Fees"
|
||||
}));
|
||||
const totalFees = (block.extras.totalFees / 100000000).toFixed(8);
|
||||
totalFeesItem.append($("<div>", {
|
||||
class: "block-detail-value",
|
||||
text: totalFees + " BTC"
|
||||
}));
|
||||
feeSection.append(totalFeesItem);
|
||||
|
||||
// Add reward
|
||||
const rewardItem = $("<div>", {
|
||||
class: "block-detail-item"
|
||||
});
|
||||
rewardItem.append($("<div>", {
|
||||
class: "block-detail-label",
|
||||
text: "Block Reward"
|
||||
}));
|
||||
const reward = (block.extras.reward / 100000000).toFixed(8);
|
||||
rewardItem.append($("<div>", {
|
||||
class: "block-detail-value",
|
||||
text: reward + " BTC"
|
||||
}));
|
||||
feeSection.append(rewardItem);
|
||||
|
||||
// Add median fee
|
||||
const medianFeeItem = $("<div>", {
|
||||
class: "block-detail-item"
|
||||
});
|
||||
medianFeeItem.append($("<div>", {
|
||||
class: "block-detail-label",
|
||||
text: "Median Fee Rate"
|
||||
}));
|
||||
medianFeeItem.append($("<div>", {
|
||||
class: "block-detail-value",
|
||||
text: block.extras.medianFee + " sat/vB"
|
||||
}));
|
||||
feeSection.append(medianFeeItem);
|
||||
|
||||
// Add average fee
|
||||
const avgFeeItem = $("<div>", {
|
||||
class: "block-detail-item"
|
||||
});
|
||||
avgFeeItem.append($("<div>", {
|
||||
class: "block-detail-label",
|
||||
text: "Average Fee"
|
||||
}));
|
||||
avgFeeItem.append($("<div>", {
|
||||
class: "block-detail-value",
|
||||
text: numberWithCommas(block.extras.avgFee) + " sat"
|
||||
}));
|
||||
feeSection.append(avgFeeItem);
|
||||
|
||||
// Add average fee rate
|
||||
const avgFeeRateItem = $("<div>", {
|
||||
class: "block-detail-item"
|
||||
});
|
||||
avgFeeRateItem.append($("<div>", {
|
||||
class: "block-detail-label",
|
||||
text: "Average Fee Rate"
|
||||
}));
|
||||
avgFeeRateItem.append($("<div>", {
|
||||
class: "block-detail-value",
|
||||
text: block.extras.avgFeeRate + " sat/vB"
|
||||
}));
|
||||
feeSection.append(avgFeeRateItem);
|
||||
|
||||
// Add fee range with visual representation
|
||||
if (block.extras.feeRange && block.extras.feeRange.length > 0) {
|
||||
const feeRangeItem = $("<div>", {
|
||||
class: "block-detail-item transaction-data"
|
||||
});
|
||||
|
||||
feeRangeItem.append($("<div>", {
|
||||
class: "block-detail-label",
|
||||
text: "Fee Rate Percentiles (sat/vB)"
|
||||
}));
|
||||
|
||||
const feeRangeText = $("<div>", {
|
||||
class: "block-detail-value",
|
||||
text: block.extras.feeRange.join(", ")
|
||||
});
|
||||
|
||||
feeRangeItem.append(feeRangeText);
|
||||
|
||||
// Add visual fee bar
|
||||
const feeBarContainer = $("<div>", {
|
||||
class: "fee-bar-container"
|
||||
});
|
||||
|
||||
const feeBar = $("<div>", {
|
||||
class: "fee-bar"
|
||||
});
|
||||
|
||||
feeBarContainer.append(feeBar);
|
||||
feeRangeItem.append(feeBarContainer);
|
||||
|
||||
// Animate the fee bar
|
||||
setTimeout(() => {
|
||||
feeBar.css("width", "100%");
|
||||
}, 100);
|
||||
|
||||
feeSection.append(feeRangeItem);
|
||||
}
|
||||
|
||||
blockDetails.append(feeSection);
|
||||
}
|
||||
|
||||
// Show the modal
|
||||
modal.css("display", "block");
|
||||
}
|
||||
|
||||
// Function to close the modal
|
||||
function closeModal() {
|
||||
$("#block-modal").css("display", "none");
|
||||
}
|
Loading…
Reference in New Issue
Block a user