feat: Implement StatsPanel component for displaying mining statistics and top miners

This commit is contained in:
MayaTheShy
2026-02-19 22:51:22 -05:00
parent 05d331efe2
commit a9bf76bd57

View File

@@ -0,0 +1,216 @@
import { useState, useEffect } from 'react';
import './StatsPanel.css';
const StatsPanel = ({ selectedTurtle, apiUrl }) => {
const [miningStats, setMiningStats] = useState([]);
const [topMiners, setTopMiners] = useState([]);
const [timeFilter, setTimeFilter] = useState(7); // days
const [loading, setLoading] = useState(false);
useEffect(() => {
loadMiningStats();
loadTopMiners();
// Refresh stats every 30 seconds
const interval = setInterval(() => {
loadMiningStats();
loadTopMiners();
}, 30000);
return () => clearInterval(interval);
}, [selectedTurtle, timeFilter]);
const loadMiningStats = async () => {
setLoading(true);
try {
const url = selectedTurtle
? `${apiUrl}/api/stats/mining/${selectedTurtle.turtleID}?days=${timeFilter}`
: `${apiUrl}/api/stats/mining?days=${timeFilter}`;
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
setMiningStats(Array.isArray(data) ? data : [data]);
}
} catch (error) {
console.error('Failed to load mining stats:', error);
} finally {
setLoading(false);
}
};
const loadTopMiners = async () => {
try {
const response = await fetch(`${apiUrl}/api/stats/top-miners?limit=10`);
if (response.ok) {
const data = await response.json();
setTopMiners(data);
}
} catch (error) {
console.error('Failed to load top miners:', error);
}
};
const getTotalBlocks = (stats) => {
if (!stats || !stats.blocks) return 0;
return stats.blocks.reduce((sum, block) => sum + block.count, 0);
};
const getBlockEmoji = (blockType) => {
if (!blockType) return '📦';
const lower = blockType.toLowerCase();
if (lower.includes('diamond')) return '💎';
if (lower.includes('gold')) return '🟡';
if (lower.includes('iron')) return '⚪';
if (lower.includes('coal')) return '⚫';
if (lower.includes('emerald')) return '🟢';
if (lower.includes('redstone')) return '🔴';
if (lower.includes('lapis')) return '🔵';
if (lower.includes('copper')) return '🟠';
if (lower.includes('stone')) return '🗿';
if (lower.includes('dirt')) return '🟤';
if (lower.includes('cobble')) return '🪨';
if (lower.includes('wood') || lower.includes('log')) return '🪵';
return '📦';
};
const formatBlockType = (blockType) => {
if (!blockType) return 'Unknown';
return blockType.replace('minecraft:', '').replace(/_/g, ' ')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};
return (
<div className="stats-panel">
<div className="stats-header">
<h2>📊 Mining Statistics</h2>
<div className="time-filter">
<button
className={timeFilter === 1 ? 'active' : ''}
onClick={() => setTimeFilter(1)}
>
24h
</button>
<button
className={timeFilter === 7 ? 'active' : ''}
onClick={() => setTimeFilter(7)}
>
7d
</button>
<button
className={timeFilter === 30 ? 'active' : ''}
onClick={() => setTimeFilter(30)}
>
30d
</button>
<button
className={timeFilter === 0 ? 'active' : ''}
onClick={() => setTimeFilter(0)}
>
All
</button>
</div>
</div>
{loading && <div className="loading">Loading statistics...</div>}
{/* Individual Turtle Stats */}
{selectedTurtle && miningStats.length > 0 && (
<div className="turtle-stats">
<h3>🐢 {selectedTurtle.name || `Turtle ${selectedTurtle.turtleID}`}</h3>
{miningStats.map((stat) => (
<div key={stat.turtleId} className="stat-section">
<div className="total-mined">
<div className="stat-value">{getTotalBlocks(stat)}</div>
<div className="stat-label">Total Blocks Mined</div>
</div>
{stat.blocks && stat.blocks.length > 0 && (
<div className="blocks-grid">
{stat.blocks.map((block, idx) => (
<div key={idx} className="block-stat">
<div className="block-emoji">{getBlockEmoji(block.blockType)}</div>
<div className="block-info">
<div className="block-name">{formatBlockType(block.blockType)}</div>
<div className="block-count">{block.count} blocks</div>
</div>
</div>
))}
</div>
)}
</div>
))}
</div>
)}
{/* Top Miners Leaderboard */}
<div className="leaderboard">
<h3>🏆 Top Miners</h3>
<div className="leaderboard-list">
{topMiners.length === 0 && (
<div className="no-data">No mining data yet. Start mining!</div>
)}
{topMiners.map((miner, idx) => (
<div key={miner.turtleId} className={`leaderboard-item rank-${idx + 1}`}>
<div className="rank">
{idx === 0 && '🥇'}
{idx === 1 && '🥈'}
{idx === 2 && '🥉'}
{idx > 2 && `#${idx + 1}`}
</div>
<div className="miner-info">
<div className="miner-name">Turtle {miner.turtleId}</div>
<div className="miner-stats">
{miner.totalBlocks} blocks {miner.uniqueTypes} types
</div>
</div>
<div className="miner-score">{miner.totalBlocks}</div>
</div>
))}
</div>
</div>
{/* All Turtles Summary */}
{!selectedTurtle && miningStats.length > 0 && (
<div className="all-turtles-stats">
<h3>📈 All Turtles Overview</h3>
<div className="turtles-summary-grid">
{miningStats.map((stat) => (
<div key={stat.turtleId} className="turtle-summary-card">
<div className="turtle-summary-header">
<span className="turtle-id">🐢 Turtle {stat.turtleId}</span>
<span className="turtle-total">{getTotalBlocks(stat)}</span>
</div>
{stat.blocks && stat.blocks.length > 0 && (
<div className="turtle-top-blocks">
{stat.blocks.slice(0, 3).map((block, idx) => (
<div key={idx} className="mini-block-stat">
<span className="mini-emoji">{getBlockEmoji(block.blockType)}</span>
<span className="mini-count">{block.count}</span>
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
)}
{/* Empty State */}
{!loading && miningStats.length === 0 && topMiners.length === 0 && (
<div className="empty-state">
<div className="empty-icon"></div>
<div className="empty-title">No Mining Data Yet</div>
<div className="empty-text">
Start mining with your turtles to see statistics here!
</div>
</div>
)}
</div>
);
};
export default StatsPanel;