"use client"; import { useEffect, useState, useCallback } from "react"; import Link from "next/link"; import Confetti from "./Confetti"; import { loadStats, recordSolve, GameStats } from "@/lib/stats"; import { GAME_META, GameId } from "@/lib/levels"; import { todayISO } from "@/lib/rng"; import { getNextSessionGame } from "@/lib/session"; 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. */ 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")}`; } /** Haptic feedback (mobile) */ function haptic(pattern: number | number[] = 10) { try { navigator.vibrate?.(pattern); } catch { /* ignore */ } } /** Build Wordle-style share text */ function buildShareText(game: string, date: string, elapsed: number, streak: number): string { const gameMeta = (GAME_META as Record)[game]; const name = gameMeta?.name ?? game; const symbol = gameMeta?.symbol ?? "🧩"; const dateLabel = new Date(date + "T00:00:00").toLocaleDateString("fr-FR", { day: "numeric", month: "long", }); const streakLine = streak > 1 ? ` 🔥${streak}` : ""; return `Puzzle Trainer – ${dateLabel}\n${name} ${symbol} ✓ ${fmt(elapsed)}${streakLine}\nhttps://puzzles.reverdin.eu`; } function ShareButton({ game, date, elapsed, streak }: { game: string; date: string; elapsed: number; streak: number }) { const [copied, setCopied] = useState(false); const handleShare = useCallback(async () => { haptic(10); const text = buildShareText(game, date, elapsed, streak); if (navigator.share) { try { await navigator.share({ text }); return; } catch { // user cancelled or not supported } } try { await navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { /* ignore */ } }, [game, date, elapsed, streak]); return ( ); } function StatCard({ icon, label, value }: { icon: React.ReactNode; label: string; value: string }) { return (
{icon}
{value} {label}
); } // First-win congratulation messages const FIRST_WIN_MESSAGES = [ "Excellent premier puzzle ! 🎉", "Tu as le coup d'œil ! ✨", "Impressionnant pour un début !", "Premier de beaucoup d'autres ! 🌟", ]; export default function WinBanner({ game, date, elapsed }: Props) { const [stats, setStats] = useState(null); const [isPersonalRecord, setIsPersonalRecord] = useState(false); const [isFirstWin, setIsFirstWin] = useState(false); const [nextSessionGame, setNextSessionGame] = useState(null); const [firstWinMsg] = useState(() => FIRST_WIN_MESSAGES[Math.floor(Math.random() * FIRST_WIN_MESSAGES.length)] ); 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 isFirst = prevStats.total === 0; setIsFirstWin(isFirst); const s = recordSolve(game, date, elapsed); setStats(s); // Personal record: had a previous best, new time is better if (!isFirst && prevStats.bestTime > 0 && elapsed < prevStats.bestTime) { setIsPersonalRecord(true); } // Haptic celebration haptic([30, 50, 30]); // Session mode: find the next game if (isDailyDate) { setNextSessionGame(getNextSessionGame(game, date)); } }, [game, date, elapsed, isDailyDate]); const displayStats = stats ?? loadStats(game); const nextHref = isDailyDate ? `/${game}/${addOneDay(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`; const today = todayISO(); const isToday = date === today; return ( <> {/* Main banner */}
{/* Trophy + title */}
Résolu ! {isPersonalRecord && ( PR )}
{/* First-win message */} {isFirstWin && (

{firstWinMsg}

)} {/* 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 (daily + today only) */} {isDailyDate && isToday && ( )}
{/* Navigation */}
{/* Session mode: next game button takes priority */} {nextSessionGame ? ( <> {GAME_META[nextSessionGame].symbol} {GAME_META[nextSessionGame].name} Accueil ) : ( <> {prevHref && ( {prevLabel} )} {nextHref && ( {nextLabel} )} {isDailyDate && ( Entraînement )} )}
); }