Add files via upload

This commit is contained in:
DJObleezy 2025-03-25 08:18:59 -07:00 committed by GitHub
parent 6fcbfa806d
commit 8c5c39d435
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 1998 additions and 0 deletions

View 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");
}
});

View 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
View 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");
}