"use client"; import Link from "next/link"; 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(() => { if (typeof window === "undefined") return []; try { const savedHistory = localStorage.getItem(HISTORY_KEY); return savedHistory ? JSON.parse(savedHistory) : []; } catch { return []; } }); // Historique const [showHistory, setShowHistory] = useState(false); // Panneau historique visible const [isScientific, setIsScientific] = useState(false); // Mode scientifique const [isDark, setIsDark] = useState(() => { if (typeof window === "undefined") return false; try { const savedTheme = localStorage.getItem(THEME_KEY); if (savedTheme === "dark") return true; if (savedTheme === "light") return false; return window.matchMedia("(prefers-color-scheme: dark)").matches; } catch { return 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); // ── Synchroniser la classe de thème avec l'état courant ── useEffect(() => { document.documentElement.classList.toggle("dark", isDark); }, [isDark]); // ── 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", })}
))}
)}
)} {/* ── Contenu SEO ── */}

Calculatrice en ligne simple et scientifique

Cette calculatrice en ligne gratuite permet de faire vos opérations du quotidien et vos calculs plus avances, directement depuis votre navigateur. Aucun compte, aucune installation et une interface rapide sur mobile comme sur ordinateur.

Le mode simple couvre addition, soustraction, multiplication et division. Le mode scientifique ajoute sin, cos, tan, logarithmes, racine carree, puissances, factorielle et constantes pi / e. L'historique est conserve localement pour retrouver vos derniers calculs.

Questions frequentes

{[ { q: "Comment faire un calcul rapidement ?", a: "Saisissez votre expression puis appuyez sur Entree ou sur le bouton =.", }, { q: "Puis-je utiliser le clavier ?", a: "Oui. Les chiffres et operateurs sont pris en charge. Entree calcule, Echap efface et Retour arriere supprime le dernier caractere.", }, { q: "A quoi sert le mode scientifique ?", a: "Il ajoute les fonctions avancees: sin, cos, tan, log, ln, racine carree, puissances et factorielle.", }, { q: "Est-ce que l'historique est conserve ?", a: "Oui, les 50 derniers calculs sont stockes localement dans votre navigateur.", }, { q: "Puis-je reutiliser un ancien calcul ?", a: "Oui, ouvrez l'historique puis cliquez sur une ligne pour recharger l'expression et le resultat.", }, { q: "La calculatrice fonctionne-t-elle sur mobile ?", a: "Oui, l'interface est adaptee aux ecrans mobiles et ordinateurs.", }, { q: "Dois-je creer un compte ?", a: "Non, aucun compte n'est necessaire pour utiliser la calculatrice.", }, { q: "Comment contacter le proprietaire du site ?", a: "Via contact.arthurp.fr ou par email a contact@arthurp.fr.", }, ].map((item, i) => (
{item.q}

{item.a}

))}
{/* ── Footer normal en bas de page ── */}
); }