puzzle-trainer/components/LevelGrid.tsx
2026-05-23 01:05:21 +00:00

196 lines
6 KiB
TypeScript

"use client";
import Link from "next/link";
import { GameId, TOTAL_LEVELS, levelMeta, GAME_META } from "@/lib/levels";
import { GameProgress } from "@/lib/progress";
interface Props {
game: GameId;
progress: GameProgress;
currentLevel?: number;
}
const DIFF_COLORS: Record<number, { bg: string; border: string; label: string; color: string }> = {
1: { bg: "#f0fdf4", border: "#86efac", label: "Facile", color: "#16a34a" },
2: { bg: "#fefce8", border: "#fde047", label: "Normal", color: "#ca8a04" },
3: { bg: "#fff7ed", border: "#fdba74", label: "Intermédiaire", color: "#ea580c" },
4: { bg: "#fef2f2", border: "#fca5a5", label: "Difficile", color: "#dc2626" },
5: { bg: "#faf5ff", border: "#d8b4fe", label: "Expert", color: "#9333ea" },
};
function fmt(s: number): string {
return `${String(Math.floor(s / 60)).padStart(2, "0")}:${String(s % 60).padStart(2, "0")}`;
}
function LevelCell({
game,
level,
done,
isCurrent,
accent,
diffInfo,
bestTime,
}: {
game: GameId;
level: number;
done: boolean;
isCurrent: boolean;
accent: string;
diffInfo: typeof DIFF_COLORS[1];
bestTime: number;
}) {
return (
<Link
href={`/${game}/level/${level}`}
title={`Niveau ${level}${diffInfo.label}${done ? ` · ${fmt(bestTime)}` : ""}`}
style={{
aspectRatio: "1",
borderRadius: 6,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 11,
fontWeight: 600,
textDecoration: "none",
position: "relative",
...(done
? {
background: accent,
color: "#fff",
border: `1.5px solid ${accent}`,
}
: isCurrent
? {
background: diffInfo.bg,
color: accent,
border: `2px solid ${accent}`,
boxShadow: `0 0 0 3px ${accent}22`,
}
: {
background: diffInfo.bg,
color: "#9ca3af",
border: `1.5px solid ${diffInfo.border}`,
}),
}}
className={`hover:scale-105 hover:shadow-md transition-transform ${isCurrent ? "level-current" : ""}`}
>
{done ? (
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={3} strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
) : (
level
)}
</Link>
);
}
export default function LevelGrid({ game, progress, currentLevel }: Props) {
const accent = GAME_META[game].accent;
const levels = Array.from({ length: TOTAL_LEVELS }, (_, i) => i + 1);
// Group levels by difficulty
const groups: { diff: number; levels: number[] }[] = [];
let currentDiff = -1;
for (const level of levels) {
const d = levelMeta(game, level).difficulty;
if (d !== currentDiff) {
groups.push({ diff: d, levels: [level] });
currentDiff = d;
} else {
groups[groups.length - 1].levels.push(level);
}
}
// Completion stats
const completedCount = Object.keys(progress).length;
return (
<div className="w-full max-w-lg">
{/* Difficulty legend */}
<div className="flex gap-3 flex-wrap mb-5">
{[1, 2, 3, 4, 5].map(d => {
const { bg, border, label, color } = DIFF_COLORS[d];
return (
<div key={d} className="flex items-center gap-1.5 text-xs" style={{ color }}>
<span style={{
width: 12,
height: 12,
borderRadius: 3,
display: "inline-block",
background: bg,
border: `1.5px solid ${border}`,
}} />
{label}
</div>
);
})}
</div>
{/* Difficulty groups */}
<div className="flex flex-col gap-5">
{groups.map(({ diff, levels: groupLevels }) => {
const diffInfo = DIFF_COLORS[diff];
const groupDone = groupLevels.filter(l => !!progress[l]).length;
return (
<div key={diff}>
{/* Group header */}
<div className="flex items-center gap-2 mb-2">
<span
className="text-xs font-semibold uppercase tracking-wide"
style={{ color: diffInfo.color }}
>
{diffInfo.label}
</span>
<span className="text-[10px] text-gray-300 font-medium tabular-nums">
{groupDone}/{groupLevels.length}
</span>
<div className="flex-1 h-px bg-gray-100" />
</div>
{/* Grid for this difficulty group */}
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(10, 1fr)",
gap: 5,
}}
>
{groupLevels.map(level => {
const record = progress[level];
const done = !!record;
const isCurrent = level === currentLevel;
const meta = levelMeta(game, level);
return (
<LevelCell
key={level}
game={game}
level={level}
done={done}
isCurrent={isCurrent}
accent={accent}
diffInfo={DIFF_COLORS[meta.difficulty]}
bestTime={record?.bestTime ?? 0}
/>
);
})}
</div>
</div>
);
})}
</div>
{/* Footer stats */}
<p className="text-xs text-gray-400 mt-5 text-center tabular-nums">
{completedCount} / {TOTAL_LEVELS} niveaux complétés
{completedCount > 0 && (
<span className="ml-2 text-gray-300">
({Math.round((completedCount / TOTAL_LEVELS) * 100)}%)
</span>
)}
</p>
</div>
);
}