Files
remoteturtle/client/src/components/StatsPanel.jsx

219 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
// Response is either a single object (per turtle) or an array (all turtles)
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();
// Response is now a flat array of {turtleId, totalBlocks, uniqueTypes}
setTopMiners(Array.isArray(data) ? data : (data.topMiners || []));
}
} 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;