"use client"; import { useEffect, useState, useCallback } from "react"; import Link from "next/link"; import Confetti from "./Confetti"; import { loadStats, recordSolve, GameStats } from "@/lib/stats"; interface Props { game: string; date: string; elapsed: number; } function fmt(s: number) { return `${String(Math.floor(s / 60)).padStart(2, "0")}:${String(s % 60).padStart(2, "0")}`; } /** Add one calendar day to a YYYY-MM-DD string, using local-timezone arithmetic. */ function addOneDay(dateStr: string): string { const [y, m, d] = dateStr.split("-").map(Number); const next = new Date(y, m - 1, d + 1); return `${next.getFullYear()}-${String(next.getMonth() + 1).padStart(2, "0")}-${String(next.getDate()).padStart(2, "0")}`; } /** Subtract one calendar day from a YYYY-MM-DD string. */ function subOneDay(dateStr: string): string { const [y, m, d] = dateStr.split("-").map(Number); const prev = new Date(y, m - 1, d - 1); return `${prev.getFullYear()}-${String(prev.getMonth() + 1).padStart(2, "0")}-${String(prev.getDate()).padStart(2, "0")}`; } /** Find the first unsolved date after fromDate, skipping already-solved ones. */ function nextUnsolvedDate(game: string, fromDate: string): string { const { solvedDates } = loadStats(game); const solved = new Set(solvedDates ?? []); let d = addOneDay(fromDate); for (let i = 0; i < 365; i++) { if (!solved.has(d)) return d; d = addOneDay(d); } return d; } function ShareButton({ game, elapsed }: { game: string; elapsed: number }) { const [copied, setCopied] = useState(false); const handleShare = useCallback(async () => { const text = `J'ai résolu le puzzle ${game} en ${fmt(elapsed)} sur Puzzle Trainer ! 🎉`; const url = typeof window !== "undefined" ? window.location.href : ""; if (navigator.share) { try { await navigator.share({ text, url }); return; } catch { // user cancelled or not supported } } // Fallback: copy to clipboard try { await navigator.clipboard.writeText(`${text}\n${url}`); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { /* ignore */ } }, [game, elapsed]); return ( ); } function StatCard({ icon, label, value }: { icon: React.ReactNode; label: string; value: string }) { return (
{icon}
{value} {label}
); } export default function WinBanner({ game, date, elapsed }: Props) { const [stats, setStats] = useState(null); const [isPersonalRecord, setIsPersonalRecord] = useState(false); const isDailyDate = /^\d{4}-\d{2}-\d{2}$/.test(date); const levelMatch = date.match(/^level-\w+-(\d+)$/); const currentLevel = levelMatch ? parseInt(levelMatch[1]) : null; useEffect(() => { if (!isDailyDate) return; const prevStats = loadStats(game); const s = recordSolve(game, date, elapsed); setStats(s); // Personal record: had a previous best, and new time is better if (prevStats.total > 0 && prevStats.bestTime > 0 && elapsed < prevStats.bestTime) { setIsPersonalRecord(true); } }, [game, date, elapsed, isDailyDate]); const displayStats = stats ?? loadStats(game); const nextHref = isDailyDate ? `/${game}/${nextUnsolvedDate(game, date)}` : currentLevel !== null ? `/${game}/level/${currentLevel + 1}` : null; const nextLabel = isDailyDate ? "Grille suivante" : `Niveau ${(currentLevel ?? 0) + 1}`; const prevHref = isDailyDate ? `/${game}/${subOneDay(date)}` : currentLevel !== null && currentLevel > 1 ? `/${game}/level/${currentLevel - 1}` : null; const prevLabel = isDailyDate ? "Hier" : currentLevel !== null ? `Niveau ${currentLevel - 1}` : ""; const levelsHref = `/${game}/levels`; return ( <> {/* Main banner */}
{/* Trophy + title */}
Résolu ! {isPersonalRecord && ( PR )}
{/* Time */}
{fmt(elapsed)}
{/* Stats row (daily only) */} {isDailyDate && (
} label="Meilleur" value={displayStats.bestTime > 0 ? fmt(displayStats.bestTime) : "--:--"} /> } label="Série" value={`${displayStats.streak}j`} /> } label="Total" value={String(displayStats.total)} />
)} {/* Share button */} {isDailyDate && ( )}
{/* Navigation */}
{prevHref && ( {prevLabel} )} {nextHref && ( {nextLabel} )} {isDailyDate && ( Entraînement )}
); }