154 lines
8.0 KiB
TypeScript
154 lines
8.0 KiB
TypeScript
import React from 'react';
|
|
|
|
interface GameResult {
|
|
score: number;
|
|
lines: number;
|
|
level: number;
|
|
timestamp: number;
|
|
}
|
|
|
|
interface PerformanceChartProps {
|
|
history: GameResult[];
|
|
}
|
|
|
|
export const PerformanceChart: React.FC<PerformanceChartProps> = ({ history }) => {
|
|
if (history.length === 0) {
|
|
return (
|
|
<div className="bg-black/40 backdrop-blur-sm p-6 rounded-2xl shadow-2xl border border-purple-500/20">
|
|
<h3 className="text-lg font-bold text-cyan-400 mb-3">Performance Trend</h3>
|
|
<p className="text-gray-400 text-sm">No games played yet. Start playing to see improvement!</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Take last 20 games for visualization
|
|
const recentGames = history.slice(-20);
|
|
const maxScore = Math.max(...recentGames.map(g => g.score), 100);
|
|
const maxLevel = Math.max(...recentGames.map(g => g.level), 5);
|
|
|
|
// Calculate statistics
|
|
const avgScore = recentGames.reduce((sum, g) => sum + g.score, 0) / recentGames.length;
|
|
const avgLevel = recentGames.reduce((sum, g) => sum + g.level, 0) / recentGames.length;
|
|
const bestGame = recentGames.reduce((best, game) =>
|
|
game.score > best.score ? game : best
|
|
, recentGames[0]);
|
|
|
|
// Calculate trend (simple moving average)
|
|
const trendWindow = 5;
|
|
const trends = recentGames.map((_, idx) => {
|
|
const start = Math.max(0, idx - trendWindow + 1);
|
|
const window = recentGames.slice(start, idx + 1);
|
|
return window.reduce((sum, g) => sum + g.score, 0) / window.length;
|
|
});
|
|
|
|
const isImproving = trends.length > 1 && trends[trends.length - 1] > trends[0];
|
|
|
|
return (
|
|
<div className="bg-black/40 backdrop-blur-sm p-6 rounded-2xl shadow-2xl border border-purple-500/20">
|
|
<h3 className="text-lg font-bold text-cyan-400 mb-4">Performance Trend</h3>
|
|
|
|
{/* Stats Summary */}
|
|
<div className="grid grid-cols-3 gap-3 mb-4">
|
|
<div className="bg-black/30 p-3 rounded-lg border border-purple-500/10">
|
|
<div className="text-xs text-gray-400">Avg Score</div>
|
|
<div className="text-xl font-bold text-purple-400">{Math.round(avgScore)}</div>
|
|
</div>
|
|
<div className="bg-black/30 p-3 rounded-lg border border-purple-500/10">
|
|
<div className="text-xs text-gray-400">Best Score</div>
|
|
<div className="text-xl font-bold text-green-400">{bestGame.score}</div>
|
|
</div>
|
|
<div className="bg-black/30 p-3 rounded-lg border border-purple-500/10">
|
|
<div className="text-xs text-gray-400">Avg Level</div>
|
|
<div className="text-xl font-bold text-cyan-400">{avgLevel.toFixed(1)}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Trend Indicator */}
|
|
<div className="mb-4 flex items-center gap-2">
|
|
<span className="text-sm text-gray-400">Trend:</span>
|
|
<span className={`text-sm font-semibold ${isImproving ? 'text-green-400' : 'text-yellow-400'}`}>
|
|
{isImproving ? '📈 Improving' : '📊 Stable'}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Chart */}
|
|
<div className="relative h-48 bg-black/30 rounded-lg p-4 border border-gray-700">
|
|
{/* Y-axis labels */}
|
|
<div className="absolute left-0 top-0 bottom-0 w-12 flex flex-col justify-between text-xs text-gray-500">
|
|
<span>{maxScore >= 1000 ? `${(maxScore / 1000).toFixed(0)}k` : maxScore}</span>
|
|
<span>{maxScore >= 2000 ? `${(maxScore / 2000).toFixed(0)}k` : Math.round(maxScore / 2)}</span>
|
|
<span>0</span>
|
|
</div>
|
|
|
|
{/* Chart area */}
|
|
<div className="ml-12 h-full relative">
|
|
{/* Grid lines */}
|
|
<div className="absolute inset-0 flex flex-col justify-between">
|
|
{[0, 1, 2, 3, 4].map(i => (
|
|
<div key={i} className="border-t border-gray-800" />
|
|
))}
|
|
</div>
|
|
|
|
{/* Score bars */}
|
|
<div className="absolute inset-0 flex items-end justify-around gap-1">
|
|
{recentGames.map((game, idx) => {
|
|
const heightPercent = Math.max((game.score / maxScore) * 100, 2); // Minimum 2% height
|
|
const trendHeight = (trends[idx] / maxScore) * 100;
|
|
|
|
return (
|
|
<div key={idx} className="flex-1 flex flex-col items-center justify-end group relative min-w-0">
|
|
{/* Trend line marker */}
|
|
{trendHeight > 0 && (
|
|
<div
|
|
className="absolute w-full border-t-2 border-yellow-400/50"
|
|
style={{ bottom: `${trendHeight}%` }}
|
|
/>
|
|
)}
|
|
|
|
{/* Score bar */}
|
|
<div
|
|
className={`w-full rounded-t transition-all ${game.score === bestGame.score
|
|
? 'bg-gradient-to-t from-green-500 to-green-400'
|
|
: 'bg-gradient-to-t from-cyan-500 to-purple-500'
|
|
} hover:opacity-80 cursor-pointer`}
|
|
style={{ height: `${heightPercent}%`, minHeight: '4px' }}
|
|
>
|
|
{/* Tooltip on hover */}
|
|
<div className="opacity-0 group-hover:opacity-100 absolute bottom-full mb-2 left-1/2 -translate-x-1/2 bg-black/90 border border-cyan-400/50 rounded px-2 py-1 whitespace-nowrap text-xs pointer-events-none z-10">
|
|
<div className="font-bold text-cyan-400">Game #{history.length - recentGames.length + idx + 1}</div>
|
|
<div className="text-gray-300">Score: <span className="text-purple-400 font-bold">{game.score}</span></div>
|
|
<div className="text-gray-300">Level: <span className="text-green-400">{game.level}</span></div>
|
|
<div className="text-gray-300">Lines: <span className="text-cyan-400">{game.lines}</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* X-axis label */}
|
|
<div className="absolute bottom-0 left-0 right-0 text-center text-xs text-gray-500 mt-2">
|
|
Last {recentGames.length} Games
|
|
</div>
|
|
</div>
|
|
|
|
{/* Legend */}
|
|
<div className="mt-4 flex items-center gap-4 text-xs">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-3 h-3 bg-gradient-to-t from-cyan-500 to-purple-500 rounded" />
|
|
<span className="text-gray-400">Score</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-3 h-0.5 bg-yellow-400/50" />
|
|
<span className="text-gray-400">Trend (avg)</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-3 h-3 bg-gradient-to-t from-green-500 to-green-400 rounded" />
|
|
<span className="text-gray-400">Best score</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|