101 lines
4.1 KiB
TypeScript
101 lines
4.1 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import Link from "next/link";
|
|
import LevelGrid from "@/components/LevelGrid";
|
|
import { GameProgress, getGameProgress, nextLevel, allStats } from "@/lib/progress";
|
|
import { GameId, GAME_META, TOTAL_LEVELS } from "@/lib/levels";
|
|
|
|
interface Props {
|
|
game: GameId;
|
|
}
|
|
|
|
function fmt(s: number) {
|
|
return `${String(Math.floor(s / 60)).padStart(2, "0")}:${String(s % 60).padStart(2, "0")}`;
|
|
}
|
|
|
|
export default function LevelsPageShell({ game }: Props) {
|
|
const [progress, setProgress] = useState<GameProgress>({});
|
|
const [next, setNext] = useState(1);
|
|
const { name, accent } = GAME_META[game];
|
|
|
|
useEffect(() => {
|
|
const refresh = () => {
|
|
setProgress(getGameProgress(game));
|
|
setNext(nextLevel(game));
|
|
};
|
|
refresh();
|
|
window.addEventListener("focus", refresh);
|
|
document.addEventListener("visibilitychange", refresh);
|
|
return () => {
|
|
window.removeEventListener("focus", refresh);
|
|
document.removeEventListener("visibilitychange", refresh);
|
|
};
|
|
}, [game]);
|
|
|
|
const stats = allStats();
|
|
const gameStats = stats[game];
|
|
const completedCount = Object.keys(progress).length;
|
|
const pct = Math.round((completedCount / TOTAL_LEVELS) * 100);
|
|
|
|
return (
|
|
<div className="flex flex-col items-center gap-8 max-w-lg mx-auto">
|
|
|
|
{/* Header */}
|
|
<div className="w-full flex items-start justify-between">
|
|
<div>
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<Link href={`/${game}`} className="text-sm text-gray-400 hover:text-gray-600 transition-colors flex items-center gap-1">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round"><polyline points="15 18 9 12 15 6"/></svg>
|
|
{name}
|
|
</Link>
|
|
</div>
|
|
<h1 className="text-2xl font-bold text-gray-900 tracking-tight">{name} — Niveaux</h1>
|
|
<p className="text-sm text-gray-400 mt-1">100 puzzles · difficulté progressive</p>
|
|
</div>
|
|
|
|
{/* Completion ring */}
|
|
<div className="flex flex-col items-end gap-0.5 shrink-0 mt-1">
|
|
<span className="text-2xl font-black tabular-nums" style={{ color: accent }}>
|
|
{completedCount}
|
|
</span>
|
|
<span className="text-xs text-gray-400">/ {TOTAL_LEVELS}</span>
|
|
{pct > 0 && (
|
|
<span className="text-[10px] font-semibold text-gray-300">{pct}%</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats strip */}
|
|
{gameStats && gameStats.bestTime > 0 && (
|
|
<div className="w-full flex gap-3">
|
|
<div className="flex-1 flex flex-col items-center gap-0.5 px-3 py-2.5 bg-white rounded-xl border border-gray-100">
|
|
<span className="text-xs text-gray-400">Meilleur temps</span>
|
|
<span className="text-base font-bold text-gray-800 timer-mono">{fmt(gameStats.bestTime)}</span>
|
|
</div>
|
|
<div className="flex-1 flex flex-col items-center gap-0.5 px-3 py-2.5 bg-white rounded-xl border border-gray-100">
|
|
<span className="text-xs text-gray-400">Prochain</span>
|
|
<span className="text-base font-bold text-gray-800">Niv. {next}</span>
|
|
</div>
|
|
<div className="flex-1 flex flex-col items-center gap-0.5 px-3 py-2.5 bg-white rounded-xl border border-gray-100">
|
|
<span className="text-xs text-gray-400">Complétés</span>
|
|
<span className="text-base font-bold text-gray-800">{completedCount}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* CTA */}
|
|
<Link
|
|
href={`/${game}/level/${next}`}
|
|
className="w-full flex items-center justify-center gap-2 py-3 rounded-2xl text-white font-semibold text-base transition-opacity hover:opacity-90 shadow-sm"
|
|
style={{ background: accent }}
|
|
>
|
|
{completedCount === 0 ? "Commencer" : "Continuer"} — Niveau {next}
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} strokeLinecap="round"><polyline points="9 18 15 12 9 6"/></svg>
|
|
</Link>
|
|
|
|
{/* Grid */}
|
|
<LevelGrid game={game} progress={progress} currentLevel={next} />
|
|
</div>
|
|
);
|
|
}
|