91 lines
3 KiB
TypeScript
91 lines
3 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import Link from "next/link";
|
|
import { loadStats } from "@/lib/stats";
|
|
import { GameId, GAME_META } from "@/lib/levels";
|
|
|
|
interface Props {
|
|
game: GameId;
|
|
date: string;
|
|
dateLabel: string;
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
function fmt(s: number) {
|
|
return `${String(Math.floor(s / 60)).padStart(2, "0")}:${String(s % 60).padStart(2, "0")}`;
|
|
}
|
|
|
|
/**
|
|
* Wraps a daily puzzle page with:
|
|
* - Already-solved banner (if player already solved today)
|
|
* - Live date header
|
|
* - "Entraîne-toi" link to /game/levels
|
|
*/
|
|
export default function DailyPageShell({ game, date, dateLabel, children }: Props) {
|
|
const [solvedToday, setSolvedToday] = useState(false);
|
|
const [stats, setStats] = useState<ReturnType<typeof loadStats> | null>(null);
|
|
const { accent } = GAME_META[game];
|
|
|
|
useEffect(() => {
|
|
const s = loadStats(game);
|
|
setStats(s);
|
|
if (s.lastDate === date) {
|
|
setSolvedToday(true);
|
|
}
|
|
}, [game, date]);
|
|
|
|
return (
|
|
<div className="flex flex-col items-center gap-6">
|
|
{/* Header */}
|
|
<div className="text-center">
|
|
<h1 className="text-2xl font-bold text-gray-900 tracking-tight">{GAME_META[game].name}</h1>
|
|
<p className="text-sm text-gray-400 mt-1 capitalize">{dateLabel}</p>
|
|
</div>
|
|
|
|
{/* Already-solved banner */}
|
|
{solvedToday && stats && (
|
|
<div
|
|
className="w-full max-w-sm flex items-center gap-3 px-4 py-3 rounded-xl border"
|
|
style={{ background: `${accent}10`, borderColor: `${accent}30` }}
|
|
>
|
|
<div
|
|
className="w-8 h-8 rounded-full flex items-center justify-center shrink-0"
|
|
style={{ background: `${accent}20` }}
|
|
>
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={accent} strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round">
|
|
<polyline points="20 6 9 17 4 12"/>
|
|
</svg>
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-semibold text-gray-800">Déjà résolu aujourd'hui</p>
|
|
{stats.bestTime > 0 && (
|
|
<p className="text-xs text-gray-500 timer-mono">Meilleur temps : {fmt(stats.bestTime)}</p>
|
|
)}
|
|
</div>
|
|
<Link
|
|
href={`/${game}/levels`}
|
|
className="shrink-0 text-xs font-semibold px-3 py-1.5 rounded-full transition-colors"
|
|
style={{ background: `${accent}18`, color: accent }}
|
|
>
|
|
Niveaux →
|
|
</Link>
|
|
</div>
|
|
)}
|
|
|
|
{/* Board */}
|
|
{children}
|
|
|
|
{/* Footer links */}
|
|
<div className="flex items-center gap-4 text-sm text-gray-400">
|
|
<Link href={`/archive?game=${game}`} className="hover:text-gray-700 transition-colors">
|
|
Archives
|
|
</Link>
|
|
<span className="text-gray-200">·</span>
|
|
<Link href={`/${game}/levels`} className="hover:text-gray-700 transition-colors">
|
|
Entraînement
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|