feat: Implement StatsPanel component for displaying mining statistics and top miners
This commit is contained in:
216
client/src/components/StatsPanel.jsx
Normal file
216
client/src/components/StatsPanel.jsx
Normal 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;
|
||||
Reference in New Issue
Block a user