"use client"; import { useState, useEffect, useCallback, useRef } from "react"; /* ══════════════════════════════════════════════ Types ══════════════════════════════════════════════ */ /** Entrée de l'historique des calculs */ interface HistoryEntry { expression: string; result: string; steps?: string[]; timestamp: number; } /* ══════════════════════════════════════════════ Constantes ══════════════════════════════════════════════ */ const HISTORY_KEY = "calc-history"; const THEME_KEY = "calc-theme"; const MAX_HISTORY = 50; /* ══════════════════════════════════════════════ Utilitaires mathématiques ══════════════════════════════════════════════ */ /** Calcule la factorielle d'un entier positif */ function factorial(n: number): number { if (n < 0) throw new Error("Factorielle non définie pour les nombres négatifs"); if (n > 170) throw new Error("Nombre trop grand pour la factorielle"); if (!Number.isInteger(n)) throw new Error("La factorielle nécessite un entier"); if (n === 0 || n === 1) return 1; let result = 1; for (let i = 2; i <= n; i++) result *= i; return result; } /** * Évalue une expression mathématique de manière sécurisée. * Retourne le résultat et les étapes de calcul. */ function evaluateExpression(expr: string): { result: number; steps: string[] } { const steps: string[] = []; let processed = expr; // Étape 1 : Remplacer les constantes if (processed.includes("π") || processed.includes("e")) { processed = processed.replace(/π/g, `(${Math.PI})`); // Remplacer 'e' seulement quand c'est la constante (pas dans 'exp' etc.) processed = processed.replace(/(? number> = { sin: Math.sin, cos: Math.cos, tan: Math.tan, log: Math.log10, ln: Math.log, sqrt: Math.sqrt, "√": Math.sqrt, }; for (const [name, fn] of Object.entries(funcMap)) { const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const regex = new RegExp(`${escapedName}\\(([^()]+)\\)`, "g"); let funcMatch; while ((funcMatch = regex.exec(processed)) !== null) { const inner = funcMatch[1]; // Évaluer l'expression interne d'abord const innerResult = safeEval(inner); const funcResult = fn(innerResult); processed = processed.replace(funcMatch[0], `(${funcResult})`); steps.push(`${name}(${inner}) = ${formatNumber(funcResult)}`); regex.lastIndex = 0; // Recommencer la recherche } } // Étape 5 : Traiter les puissances (^) processed = processed.replace(/\^/g, "**"); // Étape 6 : Évaluer l'expression finale steps.push(`Expression finale : ${processed}`); const result = safeEval(processed); // Vérifier les erreurs if (!isFinite(result)) { if (isNaN(result)) throw new Error("Résultat indéfini"); throw new Error("Division par zéro"); } return { result, steps }; } /** Évalue une expression arithmétique de manière sécurisée (sans eval) */ function safeEval(expr: string): number { // Vérifier que l'expression ne contient que des caractères autorisés const sanitized = expr.replace(/\s/g, ""); if (!/^[0-9+\-*/().e]+$/i.test(sanitized)) { throw new Error("Expression invalide"); } // Utiliser Function au lieu d'eval pour un scope isolé try { const fn = new Function(`"use strict"; return (${sanitized});`); return fn(); } catch { throw new Error("Expression invalide"); } } /** Formate un nombre pour l'affichage */ function formatNumber(n: number): string { if (Number.isInteger(n) && Math.abs(n) < 1e15) { return n.toString(); } // Arrondir à 10 décimales max pour éviter les erreurs de flottant const rounded = parseFloat(n.toPrecision(12)); if (Math.abs(rounded) < 1e-10 && rounded !== 0) { return rounded.toExponential(4); } if (Math.abs(rounded) >= 1e15) { return rounded.toExponential(4); } return rounded.toString(); } /* ══════════════════════════════════════════════ Composant principal : Calculator ══════════════════════════════════════════════ */ export default function Calculator() { // ── État ── const [expression, setExpression] = useState(""); // Expression en cours const [result, setResult] = useState(""); // Résultat calculé const [error, setError] = useState(""); // Message d'erreur const [steps, setSteps] = useState([]); // Étapes de calcul const [showSteps, setShowSteps] = useState(false); // Afficher les étapes const [history, setHistory] = useState([]); // Historique const [showHistory, setShowHistory] = useState(false); // Panneau historique visible const [isScientific, setIsScientific] = useState(false); // Mode scientifique const [isDark, setIsDark] = useState(false); // Thème sombre const [copied, setCopied] = useState(false); // Feedback copie const [lastKey, setLastKey] = useState(""); // Dernière touche pressée (feedback visuel) const displayRef = useRef(null); const simpleRef = useRef(null); const sciRef = useRef(null); // ── Chargement initial depuis localStorage ── useEffect(() => { try { const savedHistory = localStorage.getItem(HISTORY_KEY); if (savedHistory) setHistory(JSON.parse(savedHistory)); const savedTheme = localStorage.getItem(THEME_KEY); if (savedTheme === "dark") { setIsDark(true); document.documentElement.classList.add("dark"); } else if (savedTheme === "light") { setIsDark(false); document.documentElement.classList.remove("dark"); } else { // Détecter la préférence système const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; setIsDark(prefersDark); } } catch { // localStorage non disponible } }, []); // ── Sauvegarder l'historique dans localStorage ── useEffect(() => { try { localStorage.setItem(HISTORY_KEY, JSON.stringify(history)); } catch { // localStorage non disponible } }, [history]); // ── Basculer le thème ── const toggleTheme = useCallback(() => { setIsDark((prev) => { const next = !prev; if (next) { document.documentElement.classList.add("dark"); localStorage.setItem(THEME_KEY, "dark"); } else { document.documentElement.classList.remove("dark"); localStorage.setItem(THEME_KEY, "light"); } return next; }); }, []); // ── Ajouter un caractère à l'expression ── const append = useCallback((value: string) => { setError(""); setResult(""); setSteps([]); setShowSteps(false); setExpression((prev) => prev + value); }, []); // ── Effacer tout ── const clear = useCallback(() => { setExpression(""); setResult(""); setError(""); setSteps([]); setShowSteps(false); }, []); // ── Supprimer le dernier caractère ── const backspace = useCallback(() => { setError(""); setExpression((prev) => { // Vérifier si on doit supprimer un mot-clé entier (sin(, cos(, etc.) const keywords = ["sin(", "cos(", "tan(", "log(", "ln(", "sqrt(", "√("]; for (const kw of keywords) { if (prev.endsWith(kw)) { return prev.slice(0, -kw.length); } } return prev.slice(0, -1); }); }, []); // ── Calculer le résultat ── const calculate = useCallback(() => { if (!expression.trim()) return; try { const { result: numResult, steps: calcSteps } = evaluateExpression(expression); const formatted = formatNumber(numResult); setResult(formatted); setSteps(calcSteps); setError(""); // Ajouter à l'historique const entry: HistoryEntry = { expression, result: formatted, steps: calcSteps, timestamp: Date.now(), }; setHistory((prev) => [entry, ...prev].slice(0, MAX_HISTORY)); } catch (err) { setError(err instanceof Error ? err.message : "Erreur de calcul"); setResult(""); setSteps([]); } }, [expression]); // ── Copier le résultat ── const copyResult = useCallback(async () => { const textToCopy = result || expression; if (!textToCopy) return; try { await navigator.clipboard.writeText(textToCopy); setCopied(true); setTimeout(() => setCopied(false), 1500); } catch { // Fallback const textarea = document.createElement("textarea"); textarea.value = textToCopy; document.body.appendChild(textarea); textarea.select(); document.execCommand("copy"); document.body.removeChild(textarea); setCopied(true); setTimeout(() => setCopied(false), 1500); } }, [result, expression]); // ── Effacer l'historique ── const clearHistory = useCallback(() => { setHistory([]); try { localStorage.removeItem(HISTORY_KEY); } catch { // ignore } }, []); // ── Charger une entrée de l'historique ── const loadFromHistory = useCallback((entry: HistoryEntry) => { setExpression(entry.expression); setResult(entry.result); setSteps(entry.steps || []); setError(""); }, []); // ── Raccourcis clavier ── useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // Ignorer si un input est focus if ( document.activeElement instanceof HTMLInputElement || document.activeElement instanceof HTMLTextAreaElement ) { return; } const key = e.key; setLastKey(key); setTimeout(() => setLastKey(""), 150); // Chiffres et opérateurs if (/^[0-9]$/.test(key)) { e.preventDefault(); append(key); } else if (key === "+" || key === "-") { e.preventDefault(); append(key); } else if (key === "*") { e.preventDefault(); append("×"); } else if (key === "/") { e.preventDefault(); append("÷"); } else if (key === ".") { e.preventDefault(); append("."); } else if (key === "(" || key === ")") { e.preventDefault(); append(key); } else if (key === "^") { e.preventDefault(); append("^"); } else if (key === "!" ) { e.preventDefault(); append("!"); } else if (key === "Enter" || key === "=") { e.preventDefault(); calculate(); } else if (key === "Backspace") { e.preventDefault(); backspace(); } else if (key === "Escape" || key === "Delete") { e.preventDefault(); clear(); } else if (key === "c" && (e.ctrlKey || e.metaKey)) { // Ctrl+C copie le résultat if (result) { e.preventDefault(); copyResult(); } } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [append, calculate, backspace, clear, copyResult, result]); // ── Scroll automatique de l'affichage ── useEffect(() => { if (displayRef.current) { displayRef.current.scrollLeft = displayRef.current.scrollWidth; } }, [expression]); /* ════════════════════════════════════════════ Boutons de la calculatrice ════════════════════════════════════════════ */ // Boutons du mode simple const simpleButtons = [ { label: "C", action: clear, style: "calc-btn-clear" }, { label: "(", action: () => append("("), style: "calc-btn-op" }, { label: ")", action: () => append(")"), style: "calc-btn-op" }, { label: "÷", action: () => append("÷"), style: "calc-btn-op" }, { label: "7", action: () => append("7"), style: "calc-btn-num" }, { label: "8", action: () => append("8"), style: "calc-btn-num" }, { label: "9", action: () => append("9"), style: "calc-btn-num" }, { label: "×", action: () => append("×"), style: "calc-btn-op" }, { label: "4", action: () => append("4"), style: "calc-btn-num" }, { label: "5", action: () => append("5"), style: "calc-btn-num" }, { label: "6", action: () => append("6"), style: "calc-btn-num" }, { label: "−", action: () => append("-"), style: "calc-btn-op" }, { label: "1", action: () => append("1"), style: "calc-btn-num" }, { label: "2", action: () => append("2"), style: "calc-btn-num" }, { label: "3", action: () => append("3"), style: "calc-btn-num" }, { label: "+", action: () => append("+"), style: "calc-btn-op" }, { label: "⌫", action: backspace, style: "calc-btn-clear" }, { label: "0", action: () => append("0"), style: "calc-btn-num" }, { label: ".", action: () => append("."), style: "calc-btn-num" }, { label: "=", action: calculate, style: "calc-btn-equal" }, ]; // Boutons scientifiques supplémentaires const scientificButtons = [ { label: "sin", action: () => append("sin("), style: "calc-btn-sci" }, { label: "cos", action: () => append("cos("), style: "calc-btn-sci" }, { label: "tan", action: () => append("tan("), style: "calc-btn-sci" }, { label: "π", action: () => append("π"), style: "calc-btn-sci" }, { label: "log", action: () => append("log("), style: "calc-btn-sci" }, { label: "ln", action: () => append("ln("), style: "calc-btn-sci" }, { label: "√", action: () => append("sqrt("), style: "calc-btn-sci" }, { label: "e", action: () => append("e"), style: "calc-btn-sci" }, { label: "x²", action: () => append("^2"), style: "calc-btn-sci" }, { label: "xⁿ", action: () => append("^"), style: "calc-btn-sci" }, { label: "n!", action: () => append("!"), style: "calc-btn-sci" }, { label: "( )", action: () => { // Insertion intelligente de parenthèses const open = (expression.match(/\(/g) || []).length; const close = (expression.match(/\)/g) || []).length; append(open > close ? ")" : "("); }, style: "calc-btn-sci" }, ]; /* ════════════════════════════════════════════ Rendu ════════════════════════════════════════════ */ return (
{/* Lien d'accessibilité : aller au contenu principal */} Aller à la calculatrice {/* ── En-tête ── */}

Calculatrice en ligne gratuite

{/* Icône soleil/lune */} {isDark ? "🌙" : "☀️"}
{/* Toggle Simple / Scientifique */}
{/* Bouton historique */}
{/* ── Corps principal ── */}
{/* ── Affichage ── */}
{/* Expression en cours */}
{expression || 0}
{/* Résultat */}
{error ? ( {error} ) : result ? ( {result} ) : ( 0 )} {/* Bouton copier */} {(result || expression) && ( )}
{/* ── Étapes de calcul ── */} {steps.length > 0 && result && (
{showSteps && (
{steps.map((step, i) => (
{step}
))}
)}
)} {/* ── Boutons scientifiques ── */} {isScientific && (
{scientificButtons.map((btn) => ( ))}
)} {/* ── Boutons principaux ── */}
{simpleButtons.map((btn) => ( ))}
{/* ── Raccourcis clavier (info) ── */}
{/* ── Panneau historique (slide-in) ── */} {showHistory && (
{ if (e.target === e.currentTarget) setShowHistory(false); }} > {/* Fond semi-transparent */}
{/* Panneau */}

Historique

{history.length > 0 && ( )}
{history.length === 0 ? (

Aucun calcul pour l'instant

Vos calculs apparaîtront ici

) : (
{history.map((entry, i) => (
{ loadFromHistory(entry); setShowHistory(false); }} >
{entry.expression}
= {entry.result}
{new Date(entry.timestamp).toLocaleString("fr-FR", { hour: "2-digit", minute: "2-digit", day: "2-digit", month: "short", })}
))}
)}
)} {/* ── Footer SEO avec contenu sémantique ── */}
); }