Files
calculatrice/app/components/Calculator.tsx
T
Puechberty Arthur 68f382d8dd first commit
2026-03-30 20:36:20 +02:00

851 lines
33 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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(/(?<![a-zA-Z])e(?![a-zA-Z0-9.])/g, `(${Math.E})`);
steps.push(`Constantes remplacées : ${processed}`);
}
// Étape 2 : Remplacer les opérateurs visuels
processed = processed.replace(/×/g, "*").replace(/÷/g, "/");
// Étape 3 : Traiter les factorielles (ex: 5!)
const factRegex = /(\d+)!/g;
let match;
while ((match = factRegex.exec(processed)) !== null) {
const n = parseInt(match[1]);
const result = factorial(n);
processed = processed.replace(match[0], result.toString());
steps.push(`${n}! = ${result}`);
}
// Étape 4 : Traiter les fonctions scientifiques
const funcMap: Record<string, (x: number) => 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<string[]>([]); // Étapes de calcul
const [showSteps, setShowSteps] = useState(false); // Afficher les étapes
const [history, setHistory] = useState<HistoryEntry[]>([]); // 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<HTMLDivElement>(null);
const simpleRef = useRef<HTMLButtonElement>(null);
const sciRef = useRef<HTMLButtonElement>(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 (
<div className="min-h-screen flex flex-col items-center justify-center p-4 sm:p-8 transition-colors duration-300">
{/* Lien d'accessibilité : aller au contenu principal */}
<a
href="#calculatrice"
className="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-50 focus:px-4 focus:py-2 focus:rounded-lg focus:bg-[var(--primary)] focus:text-white"
>
Aller à la calculatrice
</a>
{/* ── En-tête ── */}
<header className="w-full max-w-lg mb-6 animate-fade-in">
<div className="flex items-center justify-between">
<h1 className="text-2xl sm:text-3xl font-bold tracking-tight">
<span className="text-[var(--primary)]">Calc</span>ulatrice en ligne gratuite
</h1>
<div className="flex items-center gap-3">
{/* Icône soleil/lune */}
<span className="text-sm">{isDark ? "🌙" : "☀️"}</span>
<button
onClick={toggleTheme}
className="theme-toggle"
aria-label="Basculer le thème"
title="Basculer thème clair/sombre"
/>
</div>
</div>
{/* Toggle Simple / Scientifique */}
<div className="mt-4 flex items-center gap-4">
<div className="mode-toggle relative">
<div
className="mode-slider"
style={{
left: isScientific
? (simpleRef.current?.offsetWidth ?? 80) + 2 + "px"
: "2px",
width: isScientific
? (sciRef.current?.offsetWidth ?? 100) + "px"
: (simpleRef.current?.offsetWidth ?? 80) + "px",
}}
/>
<button
ref={simpleRef}
className={!isScientific ? "active" : ""}
onClick={() => setIsScientific(false)}
>
Simple
</button>
<button
ref={sciRef}
className={isScientific ? "active" : ""}
onClick={() => setIsScientific(true)}
>
Scientifique
</button>
</div>
{/* Bouton historique */}
<button
onClick={() => setShowHistory(!showHistory)}
className="ml-auto flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors"
style={{
background: showHistory ? "var(--primary)" : "var(--surface)",
color: showHistory ? "white" : "var(--muted)",
border: `1px solid ${showHistory ? "transparent" : "var(--border-color)"}`,
}}
title="Historique des calculs"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
Historique
</button>
</div>
</header>
{/* ── Corps principal ── */}
<main
id="calculatrice"
className="w-full max-w-lg animate-fade-in"
style={{ animationDelay: "0.1s" }}
role="application"
aria-label="Calculatrice"
>
<div
className="rounded-2xl overflow-hidden transition-shadow duration-300"
style={{
background: "var(--surface)",
boxShadow: "var(--shadow-lg)",
border: "1px solid var(--border-color)",
}}
>
{/* ── Affichage ── */}
<div className="calc-display m-3 sm:m-4 p-4 sm:p-5" role="region" aria-label="Écran de la calculatrice">
{/* Expression en cours */}
<div
ref={displayRef}
className="overflow-x-auto whitespace-nowrap text-right font-mono text-lg sm:text-xl min-h-[28px] custom-scrollbar"
style={{ color: "var(--muted)" }}
aria-label="Expression en cours"
aria-live="polite"
>
{expression || <span className="opacity-40">0</span>}
</div>
{/* Résultat */}
<div
className="text-right font-mono text-3xl sm:text-4xl font-bold mt-2 min-h-[44px] flex items-center justify-end gap-2"
aria-label={error ? `Erreur : ${error}` : `Résultat : ${result || '0'}`}
aria-live="assertive"
role="status"
>
{error ? (
<span className="text-red-500 text-lg sm:text-xl animate-fade-in">{error}</span>
) : result ? (
<span className="animate-fade-in">{result}</span>
) : (
<span className="opacity-20">0</span>
)}
{/* Bouton copier */}
{(result || expression) && (
<button
onClick={copyResult}
className="p-1.5 rounded-lg transition-colors hover:bg-[var(--surface-hover)]"
title="Copier le résultat"
>
{copied ? (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12" />
</svg>
) : (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--muted)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
)}
</button>
)}
</div>
</div>
{/* ── Étapes de calcul ── */}
{steps.length > 0 && result && (
<div className="mx-3 sm:mx-4 mb-2">
<button
onClick={() => setShowSteps(!showSteps)}
className="text-xs font-medium flex items-center gap-1 px-2 py-1 rounded-md transition-colors"
style={{ color: "var(--primary)" }}
>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
style={{
transform: showSteps ? "rotate(90deg)" : "rotate(0)",
transition: "transform 0.2s ease",
}}
>
<polyline points="9 18 15 12 9 6" />
</svg>
Voir les étapes
</button>
{showSteps && (
<div className="mt-1 p-3 rounded-lg text-xs font-mono space-y-1 animate-fade-in" style={{ background: "var(--display-bg)" }}>
{steps.map((step, i) => (
<div key={i} style={{ color: "var(--muted)" }}>
<span style={{ color: "var(--primary)" }}></span> {step}
</div>
))}
</div>
)}
</div>
)}
{/* ── Boutons scientifiques ── */}
{isScientific && (
<div
className="grid grid-cols-4 gap-2 px-3 sm:px-4 pb-2 animate-fade-in"
role="group"
aria-label="Fonctions scientifiques"
>
{scientificButtons.map((btn) => (
<button
key={btn.label}
onClick={btn.action}
className={`calc-btn ${btn.style} h-11 text-sm`}
aria-label={`Fonction ${btn.label}`}
>
{btn.label}
</button>
))}
</div>
)}
{/* ── Boutons principaux ── */}
<div
className="grid grid-cols-4 gap-2 p-3 sm:p-4 pt-2"
role="group"
aria-label="Clavier de la calculatrice"
>
{simpleButtons.map((btn) => (
<button
key={btn.label}
onClick={btn.action}
className={`calc-btn ${btn.style} h-14 sm:h-16 text-lg ${
lastKey === btn.label ? "animate-press" : ""
}`}
aria-label={btn.label === "⌫" ? "Supprimer" : btn.label === "C" ? "Tout effacer" : btn.label === "=" ? "Calculer" : btn.label === "" ? "Moins" : btn.label}
>
{btn.label}
</button>
))}
</div>
</div>
{/* ── Raccourcis clavier (info) ── */}
<nav className="mt-3 text-center" aria-label="Informations sur les raccourcis">
<p className="text-xs" style={{ color: "var(--muted)" }}>
Raccourcis : chiffres, opérateurs, Entrée = calculer, Échap = effacer, Retour = supprimer
</p>
</nav>
</main>
{/* ── Panneau historique (slide-in) ── */}
{showHistory && (
<div
className="fixed inset-0 z-50 flex justify-end"
onClick={(e) => {
if (e.target === e.currentTarget) setShowHistory(false);
}}
>
{/* Fond semi-transparent */}
<div className="absolute inset-0 bg-black/30 backdrop-blur-sm animate-fade-in" />
{/* Panneau */}
<div
className="relative w-full max-w-sm h-full overflow-y-auto custom-scrollbar p-5 animate-fade-in"
style={{
background: "var(--surface)",
borderLeft: "1px solid var(--border-color)",
boxShadow: "var(--shadow-lg)",
}}
>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold">Historique</h2>
<div className="flex items-center gap-2">
{history.length > 0 && (
<button
onClick={clearHistory}
className="text-xs px-2 py-1 rounded-md text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
>
Tout effacer
</button>
)}
<button
onClick={() => setShowHistory(false)}
className="p-1 rounded-lg hover:bg-[var(--surface-hover)] transition-colors"
aria-label="Fermer l'historique"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
</div>
{history.length === 0 ? (
<div className="text-center py-12" style={{ color: "var(--muted)" }}>
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="mx-auto mb-3 opacity-30">
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
<p className="text-sm">Aucun calcul pour l&apos;instant</p>
<p className="text-xs mt-1 opacity-60">Vos calculs apparaîtront ici</p>
</div>
) : (
<div className="space-y-2">
{history.map((entry, i) => (
<div
key={entry.timestamp + "-" + i}
className="history-item animate-fade-in"
style={{ animationDelay: `${i * 0.03}s` }}
onClick={() => {
loadFromHistory(entry);
setShowHistory(false);
}}
>
<div className="font-mono text-sm truncate" style={{ color: "var(--muted)" }}>
{entry.expression}
</div>
<div className="font-mono text-lg font-bold truncate">
= {entry.result}
</div>
<div className="text-xs mt-1" style={{ color: "var(--muted)", opacity: 0.6 }}>
{new Date(entry.timestamp).toLocaleString("fr-FR", {
hour: "2-digit",
minute: "2-digit",
day: "2-digit",
month: "short",
})}
</div>
</div>
))}
</div>
)}
</div>
</div>
)}
{/* ── Footer SEO avec contenu sémantique ── */}
<footer className="mt-12 w-full max-w-2xl animate-fade-in" style={{ animationDelay: "0.3s" }}>
{/* Contenu textuel riche pour le SEO */}
<article className="mb-8 px-4">
<h2 className="text-xl font-bold mb-3" style={{ color: "var(--foreground)" }}>
Calculatrice en ligne gratuite
</h2>
<p className="text-sm leading-relaxed mb-4" style={{ color: "var(--muted)" }}>
Notre calculatrice en ligne gratuite vous permet d&apos;effectuer tous vos calculs
directement dans votre navigateur, sans installation ni inscription. Que vous ayez
besoin d&apos;une simple addition ou d&apos;un calcul scientifique complexe avec des
fonctions trigonométriques, notre outil s&apos;adapte à vos besoins.
</p>
<h2 className="text-lg font-semibold mb-2" style={{ color: "var(--foreground)" }}>
Mode simple : les opérations essentielles
</h2>
<p className="text-sm leading-relaxed mb-4" style={{ color: "var(--muted)" }}>
Le mode simple couvre les quatre opérations fondamentales : addition (+),
soustraction (), multiplication (×) et division (÷). Vous pouvez utiliser
des parenthèses pour structurer vos expressions et obtenir des résultats
précis. La gestion des erreurs vous avertit automatiquement en cas de
division par zéro.
</p>
<h2 className="text-lg font-semibold mb-2" style={{ color: "var(--foreground)" }}>
Mode scientifique : des fonctions avancées
</h2>
<p className="text-sm leading-relaxed mb-4" style={{ color: "var(--muted)" }}>
Basculez en mode scientifique pour accéder aux fonctions trigonométriques
(sinus, cosinus, tangente), aux logarithmes (log décimal, logarithme naturel),
à la racine carrée, aux puissances, à la factorielle, ainsi qu&apos;aux
constantes mathématiques π et e. Idéal pour les étudiants, ingénieurs et
professionnels.
</p>
</article>
{/* Section FAQ pour le SEO */}
<section className="mb-8 px-4" aria-labelledby="faq-heading">
<h2 id="faq-heading" className="text-lg font-semibold mb-3" style={{ color: "var(--foreground)" }}>
Questions fréquentes
</h2>
<div className="space-y-3">
{[
{
q: "Comment utiliser la calculatrice en ligne ?",
a: "Cliquez sur les boutons ou utilisez votre clavier pour saisir une expression mathématique, puis appuyez sur « = » ou Entrée pour obtenir le résultat.",
},
{
q: "Quelles fonctions scientifiques sont disponibles ?",
a: "Sinus, cosinus, tangente, logarithme décimal, logarithme naturel, racine carrée, puissances, factorielle, et les constantes π et e.",
},
{
q: "L'historique des calculs est-il sauvegardé ?",
a: "Oui, vos 50 derniers calculs sont sauvegardés automatiquement dans votre navigateur et persistent même après fermeture de la page.",
},
{
q: "La calculatrice est-elle vraiment gratuite ?",
a: "Oui, 100 % gratuite, sans publicité et sans inscription. Utilisez-la autant que vous le souhaitez.",
},
{
q: "Puis-je utiliser des raccourcis clavier ?",
a: "Oui ! Chiffres et opérateurs au clavier, Entrée pour calculer, Échap pour effacer, Retour arrière pour supprimer, Ctrl+C pour copier.",
},
].map((faq, i) => (
<details
key={i}
className="rounded-lg overflow-hidden transition-all"
style={{
background: "var(--surface)",
border: "1px solid var(--border-color)",
}}
>
<summary
className="px-4 py-3 cursor-pointer text-sm font-medium select-none hover:bg-[var(--surface-hover)] transition-colors"
style={{ color: "var(--foreground)" }}
>
{faq.q}
</summary>
<p
className="px-4 pb-3 text-sm leading-relaxed"
style={{ color: "var(--muted)" }}
>
{faq.a}
</p>
</details>
))}
</div>
</section>
{/* Copyright */}
<div className="text-center text-xs pb-4" style={{ color: "var(--muted)" }}>
<p>© {new Date().getFullYear()} Calculatrice en ligne gratuite Simple &amp; Scientifique</p>
<p className="mt-1 opacity-60">
Outil de calcul en ligne rapide, gratuit et sans inscription.
</p>
</div>
</footer>
</div>
);
}