puzzle-trainer/app/settings/page.tsx
2026-05-23 01:05:21 +00:00

207 lines
8.8 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { getPlayMode, savePlayMode, PlayMode } from "@/lib/session";
import { GAMES } from "@/lib/levels";
import { loadStats, getRitualStreak } from "@/lib/stats";
function todayISO() {
const d = new Date();
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}
function formatTime(secs: number) {
if (!secs) return "—";
return `${Math.floor(secs / 60)}:${String(secs % 60).padStart(2, "0")}`;
}
export default function SettingsPage() {
const [mode, setMode] = useState<PlayMode>("free");
const [storageOk, setStorageOk] = useState<boolean | null>(null);
const [isIOS, setIsIOS] = useState(false);
const [isStandalone, setIsStandalone] = useState(false);
const [exportDone, setExportDone] = useState(false);
useEffect(() => {
setMode(getPlayMode());
setIsIOS(/iPhone|iPad|iPod/.test(navigator.userAgent));
setIsStandalone(window.matchMedia("(display-mode: standalone)").matches);
// Check storage persistence
if ("storage" in navigator && "persisted" in navigator.storage) {
navigator.storage.persisted().then(p => setStorageOk(p));
}
}, []);
function handleModeChange(m: PlayMode) {
setMode(m);
savePlayMode(m);
}
async function requestPersist() {
if ("storage" in navigator && "persist" in navigator.storage) {
const granted = await navigator.storage.persist();
setStorageOk(granted);
}
}
function exportData() {
const data: Record<string, unknown> = { exportDate: todayISO() };
GAMES.forEach(g => {
const raw = localStorage.getItem(`stats-${g}`);
if (raw) data[`stats-${g}`] = JSON.parse(raw);
const lvl = localStorage.getItem(`levels-${g}`);
if (lvl) data[`levels-${g}`] = JSON.parse(lvl);
});
data["pt-mode"] = localStorage.getItem("pt-mode");
data["ritual"] = localStorage.getItem("ritual-streak");
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `puzzle-trainer-${todayISO()}.json`;
a.click();
URL.revokeObjectURL(url);
setExportDone(true);
setTimeout(() => setExportDone(false), 2000);
}
const ritual = typeof window !== "undefined" ? getRitualStreak() : { streak: 0, lastDate: "" };
return (
<div className="px-4 pb-8">
{/* Header */}
<div className="pt-4 pb-5">
<h1 className="text-2xl font-bold text-gray-900">Réglages</h1>
</div>
{/* Stats summary */}
<section className="mb-5">
<h2 className="text-xs font-semibold uppercase tracking-wider text-gray-400 mb-2 px-1">Résumé</h2>
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm divide-y divide-gray-50">
<div className="flex items-center justify-between px-4 py-3">
<span className="text-sm text-gray-700">Série quotidienne</span>
<span className="text-sm font-bold text-gray-900">
{ritual.streak > 0 ? `🔥 ${ritual.streak} jour${ritual.streak > 1 ? "s" : ""}` : "—"}
</span>
</div>
{GAMES.map(g => {
const stats = typeof window !== "undefined" ? loadStats(g) : null;
return (
<div key={g} className="flex items-center justify-between px-4 py-3">
<span className="text-sm text-gray-700 capitalize">{g}</span>
<span className="text-sm text-gray-500">
{stats?.total ?? 0} résolus · meilleur {formatTime(stats?.bestTime ?? 0)}
</span>
</div>
);
})}
</div>
</section>
{/* Mode de jeu */}
<section className="mb-5">
<h2 className="text-xs font-semibold uppercase tracking-wider text-gray-400 mb-2 px-1">Mode de jeu</h2>
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm">
<button
onClick={() => handleModeChange("free")}
className="w-full flex items-center justify-between px-4 py-3.5 text-left"
>
<div>
<p className="text-sm font-medium text-gray-900">Libre</p>
<p className="text-xs text-gray-400">Joue les puzzles dans n'importe quel ordre</p>
</div>
<span className={`w-5 h-5 rounded-full border-2 flex-shrink-0 ${mode === "free" ? "border-gray-900 bg-gray-900" : "border-gray-300"}`} />
</button>
<div className="h-px bg-gray-50 mx-4" />
<button
onClick={() => handleModeChange("session")}
className="w-full flex items-center justify-between px-4 py-3.5 text-left"
>
<div>
<p className="text-sm font-medium text-gray-900">Session — 5 à la suite</p>
<p className="text-xs text-gray-400">Enchaîne tous les jeux guidé, un après l'autre</p>
</div>
<span className={`w-5 h-5 rounded-full border-2 flex-shrink-0 ${mode === "session" ? "border-gray-900 bg-gray-900" : "border-gray-300"}`} />
</button>
</div>
</section>
{/* Données */}
<section className="mb-5">
<h2 className="text-xs font-semibold uppercase tracking-wider text-gray-400 mb-2 px-1">Données</h2>
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm divide-y divide-gray-50">
<div className="flex items-center justify-between px-4 py-3.5">
<div>
<p className="text-sm font-medium text-gray-900">Stockage persistant</p>
<p className="text-xs text-gray-400">Évite la suppression des données par Safari</p>
</div>
{storageOk === true ? (
<span className="text-xs text-green-600 font-medium">Activé</span>
) : storageOk === false ? (
<button
onClick={requestPersist}
className="text-xs font-medium text-blue-600 active:opacity-70"
>
Activer
</button>
) : (
<span className="text-xs text-gray-400"></span>
)}
</div>
<button
onClick={exportData}
className="w-full flex items-center justify-between px-4 py-3.5 text-left active:opacity-70"
>
<div>
<p className="text-sm font-medium text-gray-900">Exporter mes données</p>
<p className="text-xs text-gray-400">Télécharge un fichier JSON de sauvegarde</p>
</div>
<span className="text-xs font-medium text-blue-600">
{exportDone ? "✓ Téléchargé" : "Télécharger"}
</span>
</button>
</div>
</section>
{/* iOS Install guide */}
{isIOS && !isStandalone && (
<section className="mb-5">
<h2 className="text-xs font-semibold uppercase tracking-wider text-gray-400 mb-2 px-1">Installer l'app</h2>
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-4">
<p className="text-sm font-medium text-gray-900 mb-3">Ajouter à l'écran d'accueil</p>
<div className="flex flex-col gap-3">
{[
{ n: 1, text: "Appuie sur l'icône Partager (carré avec flèche vers le haut) en bas de Safari" },
{ n: 2, text: "Fais défiler et appuie sur « Sur l'écran d'accueil »" },
{ n: 3, text: "Appuie sur « Ajouter » en haut à droite" },
].map(step => (
<div key={step.n} className="flex gap-3 items-start">
<span className="w-5 h-5 rounded-full bg-gray-900 text-white text-[10px] font-bold flex items-center justify-center flex-shrink-0 mt-0.5">{step.n}</span>
<p className="text-sm text-gray-600 leading-snug">{step.text}</p>
</div>
))}
</div>
<p className="text-xs text-gray-400 mt-3">L'app sera disponible hors connexion une fois installée.</p>
</div>
</section>
)}
{/* À propos */}
<section>
<h2 className="text-xs font-semibold uppercase tracking-wider text-gray-400 mb-2 px-1">À propos</h2>
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm divide-y divide-gray-50">
<div className="flex items-center justify-between px-4 py-3">
<span className="text-sm text-gray-700">Version</span>
<span className="text-sm text-gray-500">1.0</span>
</div>
<div className="flex items-center justify-between px-4 py-3">
<span className="text-sm text-gray-700">Données stockées</span>
<span className="text-sm text-gray-500">100% sur votre appareil</span>
</div>
</div>
</section>
</div>
);
}