"use client";
import Link from "next/link";
import { useEffect, useState } from "react";
import { todayISO } from "@/lib/rng";
import { GAME_META, GAMES, GameId } from "@/lib/levels";
import { allStats, GameStats } from "@/lib/progress";
import { loadStats, updateRitualStreak, getRitualStreak, RitualStats } from "@/lib/stats";
import { PlayMode, getPlayMode, savePlayMode, getSessionOrder } from "@/lib/session";
// ── Other helpers ─────────────────────────────────────────────────────────────
function fmt(s: number): string {
return `${String(Math.floor(s / 60)).padStart(2, "0")}:${String(s % 60).padStart(2, "0")}`;
}
function isDailySolved(game: GameId, date: string): boolean {
if (typeof window === "undefined") return false;
try {
const stats = localStorage.getItem(`stats-${game}`);
if (!stats) return false;
return JSON.parse(stats)?.lastDate === date;
} catch { return false; }
}
function isNewUser(): boolean {
if (typeof window === "undefined") return true;
return GAMES.every(g => !localStorage.getItem(`stats-${g}`));
}
// ── Onboarding screen ─────────────────────────────────────────────────────────
function OnboardingScreen() {
return (
🧩
Puzzle Trainer
5 puzzles logiques, chaque jour.
🔓
Pas de compte, pas de pub
📱
Tout dans le navigateur
⏱
2 à 4 minutes par puzzle
Commencer
Commence avec Tango — le plus facile à apprendre
);
}
// ── Mode toggle ───────────────────────────────────────────────────────────────
function ModeToggle({ mode, onChange }: { mode: PlayMode; onChange: (m: PlayMode) => void }) {
return (
);
}
// ── Streak banner ─────────────────────────────────────────────────────────────
function StreakBanner({ ritual, today }: { ritual: RitualStats; today: string }) {
if (ritual.streak === 0) return null;
const isAliveToday = ritual.lastDate === today;
return (
🔥
{ritual.streak} jour{ritual.streak > 1 ? "s" : ""} de suite
{isAliveToday
? "Tous les puzzles complétés aujourd'hui !"
: "Complète les puzzles du jour pour continuer ta série"}
);
}
// ── Free mode: simple game row ────────────────────────────────────────────────
function FreeRow({ game, solved, lastTime }: {
game: GameId;
solved: boolean;
lastTime: number;
}) {
const { name, accent, subtitle, duration, symbol } = GAME_META[game];
return (
{solved ? "✓" : symbol}
{name}
{!solved && {duration}}
{solved && lastTime > 0 ? `Résolu en ${fmt(lastTime)}` : subtitle}
{solved ? (
Rejouer
) : (
Jouer
)}
);
}
// ── Session mode: ordered list with progress ──────────────────────────────────
function SessionList({ order, solvedToday, lastTimes }: {
order: GameId[];
solvedToday: Record;
lastTimes: Record;
}) {
// Find the next game to play (first unsolved in order)
const nextIdx = order.findIndex(g => !solvedToday[g]);
const allDone = nextIdx === -1;
const solvedCount = order.filter(g => solvedToday[g]).length;
return (
{/* Progress bar */}
{solvedCount}/{order.length}
{order.map((game, idx) => {
const solved = solvedToday[game];
const isCurrent = !allDone && idx === nextIdx;
const isPending = !solved && !isCurrent;
const { name, accent, subtitle, duration, symbol } = GAME_META[game];
return (
{/* Step number / status */}
{solved ? "✓" : isCurrent ? symbol : {idx + 1}}
{/* Connector line below (not last) */}
{idx < order.length - 1 && (
)}
{name}
{!solved && isCurrent && (
{duration}
)}
{solved && lastTimes[game] > 0
? `Résolu en ${fmt(lastTimes[game])}`
: isCurrent
? subtitle
: ""}
{/* CTA */}
{solved ? (
✓
) : isCurrent ? (
Jouer
) : (
)}
);
})}
);
}
// ── All done banner ────────────────────────────────────────────────────────────
function AllDoneBanner({ nextPuzzleIn }: { nextPuzzleIn: string }) {
return (
🎉
Bravo ! Tous les puzzles du jour complétés.
Prochain puzzle dans {nextPuzzleIn}
);
}
// ── Training row ──────────────────────────────────────────────────────────────
function TrainingRow({ game, stats }: { game: GameId; stats: GameStats | undefined }) {
const { name, accent } = GAME_META[game];
const pct = stats?.pct ?? 0;
const label = !stats || stats.completed === 0 ? "Commencer" : `Niv. ${stats.nextLevel}`;
return (
{name}
{stats?.completed ?? 0}/100
{label}
);
}
// ── Main page ─────────────────────────────────────────────────────────────────
export default function Home() {
const today = todayISO();
const [loaded, setLoaded] = useState(false);
const [newUser, setNewUser] = useState(false);
const [mode, setMode] = useState("free");
const [levelStats, setLevelStats] = useState | null>(null);
const [solvedToday, setSolvedToday] = useState>({} as Record);
const [lastTimes, setLastTimes] = useState>({} as Record);
const [ritual, setRitual] = useState({ streak: 0, lastDate: "" });
const [showTraining, setShowTraining] = useState(false);
const handleModeChange = (m: PlayMode) => {
setMode(m);
savePlayMode(m);
};
useEffect(() => {
const refresh = () => {
setNewUser(isNewUser());
setMode(getPlayMode());
setLevelStats(allStats());
const solved = {} as Record;
const times = {} as Record;
for (const g of GAMES) {
solved[g] = isDailySolved(g, today);
const s = loadStats(g);
times[g] = s.lastDate === today ? s.lastTime : 0;
}
setSolvedToday(solved);
setLastTimes(times);
const allSolved = GAMES.every(g => solved[g]);
const r = allSolved ? updateRitualStreak(today) : getRitualStreak();
setRitual(r);
setLoaded(true);
};
refresh();
window.addEventListener("focus", refresh);
document.addEventListener("visibilitychange", refresh);
return () => {
window.removeEventListener("focus", refresh);
document.removeEventListener("visibilitychange", refresh);
};
}, [today]);
if (!loaded) {
return (
{[...Array(5)].map((_, i) => (
))}
);
}
if (newUser) return ;
const dateLabel = new Date(today + "T00:00:00").toLocaleDateString("fr-FR", {
weekday: "long", day: "numeric", month: "long",
});
const sessionOrder = getSessionOrder(today);
const totalSolvedToday = GAMES.filter(g => solvedToday[g]).length;
const allSolvedToday = totalSolvedToday === GAMES.length;
const now = new Date();
const midnight = new Date(now);
midnight.setDate(midnight.getDate() + 1);
midnight.setHours(0, 0, 0, 0);
const hoursLeft = Math.ceil((midnight.getTime() - now.getTime()) / 3600000);
const nextPuzzleIn = `${hoursLeft}h`;
return (
{/* Header */}
Puzzle Trainer
{dateLabel}
Stats
{/* Streak */}
{/* Daily puzzles */}
Puzzles du jour
{mode === "session" && (
Joue les 5 puzzles à la suite — l'ordre change chaque jour.
)}
{allSolvedToday ? (
) : mode === "session" ? (
) : (
{GAMES.map(game => (
))}
)}
{/* Training */}
{showTraining && (
{GAMES.map(game => (
))}
)}
{/* Footer */}
Archives
·
Statistiques
·
Comment jouer
);
}