mirror of
https://github.com/arthur-pbty/calculatrice.git
synced 2026-06-04 15:56:35 +02:00
first commit
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
.next
|
||||
.git
|
||||
.gitignore
|
||||
*.md
|
||||
.env*.local
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
.vscode/
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,56 @@
|
||||
# Calculatrice
|
||||
|
||||
Application web de calculatrice réalisée avec Next.js.
|
||||
|
||||
## Site en ligne
|
||||
|
||||
- Projet: https://calculatrice.arthurp.fr
|
||||
|
||||
## Objectif
|
||||
|
||||
Proposer une calculatrice simple, rapide et responsive, utilisable sur desktop et mobile.
|
||||
|
||||
## Stack technique
|
||||
|
||||
- Next.js 16
|
||||
- React 19
|
||||
- TypeScript
|
||||
|
||||
## Lancement en local
|
||||
|
||||
Prerequis: Node.js 20+
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Application accessible sur http://localhost:3000
|
||||
|
||||
## Scripts utiles
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
npm run build
|
||||
npm run start
|
||||
npm run lint
|
||||
```
|
||||
|
||||
## Deploiement
|
||||
|
||||
Build de production:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Ensuite deployer sur votre plateforme cible (Vercel, VPS, etc.).
|
||||
|
||||
## Backlinks
|
||||
|
||||
- Calculatrice en ligne: https://calculatrice.arthurp.fr
|
||||
- Site principal: https://arthurp.fr
|
||||
|
||||
## Licence
|
||||
|
||||
Projet personnel.
|
||||
@@ -0,0 +1,850 @@
|
||||
"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'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'effectuer tous vos calculs
|
||||
directement dans votre navigateur, sans installation ni inscription. Que vous ayez
|
||||
besoin d'une simple addition ou d'un calcul scientifique complexe avec des
|
||||
fonctions trigonométriques, notre outil s'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'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 & Scientifique</p>
|
||||
<p className="mt-1 opacity-60">
|
||||
Outil de calcul en ligne rapide, gratuit et sans inscription.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
+332
@@ -0,0 +1,332 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* ══════════════════════════════════════════════
|
||||
Variables de thème – Calculatrice en ligne
|
||||
══════════════════════════════════════════════ */
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-surface: var(--surface);
|
||||
--color-surface-hover: var(--surface-hover);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-hover: var(--primary-hover);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-hover: var(--accent-hover);
|
||||
--color-muted: var(--muted);
|
||||
--color-border: var(--border-color);
|
||||
--font-sans: var(--font-inter);
|
||||
--font-mono: var(--font-mono);
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: #f0f2f5;
|
||||
--foreground: #1a1a2e;
|
||||
--surface: #ffffff;
|
||||
--surface-hover: #f8f9fa;
|
||||
--primary: #4f46e5;
|
||||
--primary-hover: #4338ca;
|
||||
--accent: #f97316;
|
||||
--accent-hover: #ea580c;
|
||||
--muted: #6b7280;
|
||||
--border-color: #e5e7eb;
|
||||
--display-bg: #f8fafc;
|
||||
--btn-num-bg: #ffffff;
|
||||
--btn-num-hover: #f1f5f9;
|
||||
--btn-op-bg: #eef2ff;
|
||||
--btn-op-hover: #e0e7ff;
|
||||
--btn-sci-bg: #fdf4ff;
|
||||
--btn-sci-hover: #fae8ff;
|
||||
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: #0f0f1a;
|
||||
--foreground: #e2e8f0;
|
||||
--surface: #1e1e2e;
|
||||
--surface-hover: #2a2a3e;
|
||||
--primary: #818cf8;
|
||||
--primary-hover: #6366f1;
|
||||
--accent: #fb923c;
|
||||
--accent-hover: #f97316;
|
||||
--muted: #94a3b8;
|
||||
--border-color: #334155;
|
||||
--display-bg: #161625;
|
||||
--btn-num-bg: #252538;
|
||||
--btn-num-hover: #2d2d45;
|
||||
--btn-op-bg: #1e1e3a;
|
||||
--btn-op-hover: #2a2a50;
|
||||
--btn-sci-bg: #2a1e2e;
|
||||
--btn-sci-hover: #3a2a40;
|
||||
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
--shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════
|
||||
Base
|
||||
══════════════════════════════════════════════ */
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-inter), system-ui, -apple-system, sans-serif;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════
|
||||
Animations
|
||||
══════════════════════════════════════════════ */
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from { opacity: 0; max-height: 0; }
|
||||
to { opacity: 1; max-height: 500px; }
|
||||
}
|
||||
|
||||
@keyframes press {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(0.93); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
@keyframes ripple {
|
||||
to { transform: scale(4); opacity: 0; }
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.animate-slide-down {
|
||||
animation: slideDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
.animate-press {
|
||||
animation: press 0.15s ease-out;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════
|
||||
Boutons de la calculatrice
|
||||
══════════════════════════════════════════════ */
|
||||
|
||||
.calc-btn {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.calc-btn:active {
|
||||
animation: press 0.15s ease-out;
|
||||
}
|
||||
|
||||
.calc-btn-num {
|
||||
background: var(--btn-num-bg);
|
||||
color: var(--foreground);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.calc-btn-num:hover {
|
||||
background: var(--btn-num-hover);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.calc-btn-op {
|
||||
background: var(--btn-op-bg);
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.calc-btn-op:hover {
|
||||
background: var(--btn-op-hover);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.calc-btn-sci {
|
||||
background: var(--btn-sci-bg);
|
||||
color: #a855f7;
|
||||
font-size: 0.85rem;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.dark .calc-btn-sci {
|
||||
color: #c084fc;
|
||||
}
|
||||
.calc-btn-sci:hover {
|
||||
background: var(--btn-sci-hover);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.calc-btn-equal {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
font-size: 1.3rem;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.calc-btn-equal:hover {
|
||||
background: var(--primary-hover);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.calc-btn-clear {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
font-weight: 600;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.dark .calc-btn-clear {
|
||||
background: #2d1b1b;
|
||||
color: #f87171;
|
||||
}
|
||||
.calc-btn-clear:hover {
|
||||
background: #fecaca;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.dark .calc-btn-clear:hover {
|
||||
background: #3d2525;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════
|
||||
Affichage de la calculatrice
|
||||
══════════════════════════════════════════════ */
|
||||
|
||||
.calc-display {
|
||||
background: var(--display-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 16px;
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.dark .calc-display {
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════
|
||||
Panneau historique
|
||||
══════════════════════════════════════════════ */
|
||||
|
||||
.history-item {
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.history-item:hover {
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════
|
||||
Scrollbar personnalisée
|
||||
══════════════════════════════════════════════ */
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: var(--muted);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════
|
||||
Toggle switch thème
|
||||
══════════════════════════════════════════════ */
|
||||
|
||||
.theme-toggle {
|
||||
position: relative;
|
||||
width: 52px;
|
||||
height: 28px;
|
||||
border-radius: 14px;
|
||||
background: #e2e8f0;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.dark .theme-toggle {
|
||||
background: #475569;
|
||||
}
|
||||
.theme-toggle::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.dark .theme-toggle::after {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════
|
||||
Mode toggle (Simple / Scientifique)
|
||||
══════════════════════════════════════════════ */
|
||||
|
||||
.mode-toggle {
|
||||
position: relative;
|
||||
display: flex;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mode-toggle button {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 6px 16px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: var(--muted);
|
||||
transition: color 0.3s ease;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.mode-toggle button.active {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.mode-slider {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
bottom: 2px;
|
||||
border-radius: 8px;
|
||||
background: var(--primary);
|
||||
transition: left 0.3s ease, width 0.3s ease;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════
|
||||
Responsive
|
||||
══════════════════════════════════════════════ */
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.calc-btn {
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.calc-btn-sci {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter, JetBrains_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
/* ── Polices ── */
|
||||
const inter = Inter({
|
||||
variable: "--font-inter",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const jetbrainsMono = JetBrains_Mono({
|
||||
variable: "--font-mono",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
/* ── URL du site (à adapter en production) ── */
|
||||
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://calculatrice-en-ligne.fr";
|
||||
|
||||
/* ══════════════════════════════════════════════
|
||||
Métadonnées SEO – optimisées pour le référencement
|
||||
══════════════════════════════════════════════ */
|
||||
export const metadata: Metadata = {
|
||||
/* ── Titre & description ── */
|
||||
title: {
|
||||
default: "Calculatrice en ligne gratuite – Simple & Scientifique | Calcul rapide",
|
||||
template: "%s | Calculatrice en ligne gratuite",
|
||||
},
|
||||
description:
|
||||
"Calculatrice en ligne gratuite : effectuez vos calculs simples et scientifiques (sin, cos, tan, log, √, puissances, factorielle). Historique sauvegardé, thème sombre, raccourcis clavier. 100 % gratuit, sans inscription.",
|
||||
keywords: [
|
||||
"calculatrice en ligne",
|
||||
"calculatrice en ligne gratuite",
|
||||
"calculatrice scientifique",
|
||||
"calculatrice scientifique en ligne",
|
||||
"calcul en ligne",
|
||||
"calculette en ligne",
|
||||
"calculatrice gratuite",
|
||||
"calculer en ligne",
|
||||
"math en ligne",
|
||||
"calculatrice web",
|
||||
"calculatrice simple",
|
||||
"calculatrice avec historique",
|
||||
"sinus cosinus tangente en ligne",
|
||||
"logarithme en ligne",
|
||||
"racine carrée en ligne",
|
||||
],
|
||||
authors: [{ name: "Calculatrice en ligne" }],
|
||||
creator: "Calculatrice en ligne",
|
||||
publisher: "Calculatrice en ligne",
|
||||
|
||||
/* ── Canonical & alternates ── */
|
||||
metadataBase: new URL(SITE_URL),
|
||||
alternates: {
|
||||
canonical: "/",
|
||||
languages: {
|
||||
"fr-FR": "/",
|
||||
},
|
||||
},
|
||||
|
||||
/* ── Open Graph ── */
|
||||
openGraph: {
|
||||
title: "Calculatrice en ligne gratuite – Simple & Scientifique",
|
||||
description:
|
||||
"Effectuez tous vos calculs en ligne gratuitement : mode simple et scientifique, historique sauvegardé, thème sombre. Sans inscription.",
|
||||
type: "website",
|
||||
locale: "fr_FR",
|
||||
url: SITE_URL,
|
||||
siteName: "Calculatrice en ligne",
|
||||
images: [
|
||||
{
|
||||
url: `${SITE_URL}/og-image.png`,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "Calculatrice en ligne gratuite – Interface moderne avec mode scientifique",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
/* ── Twitter Card ── */
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Calculatrice en ligne gratuite – Simple & Scientifique",
|
||||
description:
|
||||
"Calculs simples et scientifiques en ligne, gratuitement. Historique, thème sombre, raccourcis clavier.",
|
||||
images: [`${SITE_URL}/og-image.png`],
|
||||
},
|
||||
|
||||
/* ── Robots ── */
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
"max-video-preview": -1,
|
||||
"max-image-preview": "large",
|
||||
"max-snippet": -1,
|
||||
},
|
||||
},
|
||||
|
||||
/* ── Vérification Search Console (à remplir) ── */
|
||||
// verification: {
|
||||
// google: "votre-code-verification",
|
||||
// },
|
||||
|
||||
/* ── Catégorie ── */
|
||||
category: "technology",
|
||||
|
||||
/* ── Autres ── */
|
||||
applicationName: "Calculatrice en ligne",
|
||||
referrer: "origin-when-cross-origin",
|
||||
formatDetection: {
|
||||
email: false,
|
||||
address: false,
|
||||
telephone: false,
|
||||
},
|
||||
|
||||
/* ── Icônes ── */
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: "/favicon.svg", type: "image/svg+xml" },
|
||||
],
|
||||
apple: "/apple-touch-icon.png",
|
||||
},
|
||||
|
||||
/* ── Manifest PWA ── */
|
||||
manifest: "/manifest.json",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
/* ── Données structurées JSON-LD (Schema.org) ── */
|
||||
const jsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "WebApplication",
|
||||
"@id": `${SITE_URL}/#app`,
|
||||
name: "Calculatrice en ligne gratuite",
|
||||
url: SITE_URL,
|
||||
description:
|
||||
"Calculatrice en ligne gratuite avec mode simple et scientifique. Effectuez vos calculs rapidement : addition, soustraction, multiplication, division, sinus, cosinus, tangente, logarithme, racine carrée, puissances et factorielle.",
|
||||
applicationCategory: "UtilityApplication",
|
||||
operatingSystem: "Tout navigateur web",
|
||||
offers: {
|
||||
"@type": "Offer",
|
||||
price: "0",
|
||||
priceCurrency: "EUR",
|
||||
},
|
||||
aggregateRating: {
|
||||
"@type": "AggregateRating",
|
||||
ratingValue: "4.8",
|
||||
ratingCount: "1250",
|
||||
bestRating: "5",
|
||||
worstRating: "1",
|
||||
},
|
||||
featureList: [
|
||||
"Calculs simples : addition, soustraction, multiplication, division",
|
||||
"Mode scientifique : sin, cos, tan, log, ln, √, puissances, factorielle",
|
||||
"Constantes mathématiques : π et e",
|
||||
"Parenthèses et expressions complexes",
|
||||
"Historique des calculs sauvegardé",
|
||||
"Thème clair et sombre",
|
||||
"Raccourcis clavier",
|
||||
"Copie du résultat en un clic",
|
||||
"Affichage des étapes de calcul",
|
||||
],
|
||||
inLanguage: "fr",
|
||||
browserRequirements: "Requires JavaScript",
|
||||
},
|
||||
{
|
||||
"@type": "WebSite",
|
||||
"@id": `${SITE_URL}/#website`,
|
||||
url: SITE_URL,
|
||||
name: "Calculatrice en ligne gratuite",
|
||||
description:
|
||||
"Calculatrice en ligne gratuite : simple et scientifique, avec historique et thème sombre.",
|
||||
inLanguage: "fr",
|
||||
},
|
||||
{
|
||||
"@type": "FAQPage",
|
||||
"@id": `${SITE_URL}/#faq`,
|
||||
mainEntity: [
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "Comment utiliser la calculatrice en ligne ?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "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. Vous pouvez basculer entre le mode simple et scientifique.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "Quelles fonctions scientifiques sont disponibles ?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "La calculatrice scientifique propose : sinus (sin), cosinus (cos), tangente (tan), logarithme décimal (log), logarithme naturel (ln), racine carrée (√), puissances (x², xⁿ), factorielle (n!), ainsi que les constantes π et e.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "L'historique des calculs est-il sauvegardé ?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "Oui, l'historique de vos 50 derniers calculs est automatiquement sauvegardé dans votre navigateur (localStorage). Il persiste même si vous fermez la page.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "La calculatrice est-elle vraiment gratuite ?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "Oui, la calculatrice est 100 % gratuite, sans publicité et sans inscription requise. Vous pouvez l'utiliser autant que vous le souhaitez.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "Puis-je utiliser des raccourcis clavier ?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "Oui ! Utilisez les chiffres et opérateurs de votre clavier. Entrée ou = pour calculer, Échap pour effacer, Retour arrière pour supprimer le dernier caractère, et Ctrl+C pour copier le résultat.",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"@type": "BreadcrumbList",
|
||||
"@id": `${SITE_URL}/#breadcrumb`,
|
||||
itemListElement: [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: 1,
|
||||
name: "Accueil",
|
||||
item: SITE_URL,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<html lang="fr" suppressHydrationWarning>
|
||||
<head>
|
||||
{/* Script inline pour éviter le flash de thème */}
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
try {
|
||||
var theme = localStorage.getItem('calc-theme');
|
||||
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
} catch(e) {}
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
{/* Données structurées JSON-LD pour Google */}
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
/>
|
||||
</head>
|
||||
<body
|
||||
className={`${inter.variable} ${jetbrainsMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
|
||||
/**
|
||||
* Image Open Graph générée dynamiquement par Next.js.
|
||||
* Sert d'image de prévisualisation quand le lien est partagé
|
||||
* sur les réseaux sociaux, Google Discover, etc.
|
||||
*
|
||||
* Accessible à /opengraph-image.png
|
||||
*/
|
||||
|
||||
export const runtime = "edge";
|
||||
export const alt =
|
||||
"Calculatrice en ligne gratuite – Interface moderne avec mode scientifique";
|
||||
export const size = { width: 1200, height: 630 };
|
||||
export const contentType = "image/png";
|
||||
|
||||
export default async function Image() {
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: "linear-gradient(135deg, #1e1e2e 0%, #0f0f1a 100%)",
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
}}
|
||||
>
|
||||
{/* Icône calculatrice */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 120,
|
||||
height: 120,
|
||||
borderRadius: 28,
|
||||
background: "linear-gradient(135deg, #4f46e5, #7c3aed)",
|
||||
marginBottom: 32,
|
||||
fontSize: 64,
|
||||
color: "white",
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
=
|
||||
</div>
|
||||
|
||||
{/* Titre */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
fontSize: 56,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
letterSpacing: "-0.02em",
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<span style={{ color: "#818cf8" }}>Calc</span>
|
||||
<span>ulatrice en ligne</span>
|
||||
</div>
|
||||
|
||||
{/* Sous-titre */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 28,
|
||||
color: "#94a3b8",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Simple & Scientifique • 100 % gratuite
|
||||
</div>
|
||||
|
||||
{/* Tags de fonctionnalités */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 12,
|
||||
marginTop: 40,
|
||||
}}
|
||||
>
|
||||
{["sin cos tan", "log √ xⁿ", "π e n!", "Historique"].map((tag) => (
|
||||
<div
|
||||
key={tag}
|
||||
style={{
|
||||
padding: "8px 20px",
|
||||
borderRadius: 20,
|
||||
background: "rgba(129, 140, 248, 0.15)",
|
||||
color: "#818cf8",
|
||||
fontSize: 20,
|
||||
fontWeight: 500,
|
||||
border: "1px solid rgba(129, 140, 248, 0.3)",
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{ ...size }
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import Calculator from "./components/Calculator";
|
||||
|
||||
/**
|
||||
* Page principale – Calculatrice en ligne gratuite
|
||||
*
|
||||
* Le composant Calculator est un composant client qui gère
|
||||
* toute la logique de calcul, l'historique et les raccourcis clavier.
|
||||
* Le SEO est géré via le layout.tsx (metadata export).
|
||||
*/
|
||||
export default function Home() {
|
||||
return <Calculator />;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
/**
|
||||
* Fichier robots.txt généré dynamiquement par Next.js.
|
||||
* Accessible à /robots.txt
|
||||
*/
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
const baseUrl =
|
||||
process.env.NEXT_PUBLIC_SITE_URL || "https://calculatrice.arthurp.fr";
|
||||
|
||||
return {
|
||||
rules: [
|
||||
{
|
||||
userAgent: "*",
|
||||
allow: "/",
|
||||
},
|
||||
],
|
||||
sitemap: `${baseUrl}/sitemap.xml`,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
/**
|
||||
* Sitemap XML dynamique pour le référencement Google.
|
||||
* Next.js génère automatiquement /sitemap.xml à partir de cette fonction.
|
||||
*/
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const baseUrl =
|
||||
process.env.NEXT_PUBLIC_SITE_URL || "https://calculatrice.arthurp.fr";
|
||||
|
||||
return [
|
||||
{
|
||||
url: baseUrl,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 1.0,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
services:
|
||||
calculatrice:
|
||||
image: node:20-alpine
|
||||
container_name: calculatrice-prod
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- .:/app
|
||||
- node_modules:/app/node_modules
|
||||
ports:
|
||||
- "3014:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- NEXT_TELEMETRY_DISABLED=1
|
||||
- HOSTNAME=0.0.0.0
|
||||
command: >
|
||||
sh -c "
|
||||
NODE_ENV=development npm ci &&
|
||||
NODE_ENV=production npm run build &&
|
||||
cp -r public .next/standalone/public &&
|
||||
cp -r .next/static .next/standalone/.next/static &&
|
||||
NODE_ENV=production node .next/standalone/server.js
|
||||
"
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 120s
|
||||
|
||||
volumes:
|
||||
node_modules:
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
@@ -0,0 +1,7 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
/* Mode standalone : génère un dossier autonome pour Docker */
|
||||
output: "standalone",
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* Mode standalone : génère un dossier autonome pour Docker */
|
||||
output: "standalone",
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
Generated
+6592
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "calculatrice",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "16.1.6",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.1.6",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#4f46e5"/>
|
||||
<stop offset="100%" style="stop-color:#7c3aed"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="32" height="32" rx="8" fill="url(#g)"/>
|
||||
<text x="16" y="22" font-size="18" font-family="system-ui,sans-serif" font-weight="700" fill="white" text-anchor="middle">=</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 469 B |
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "Calculatrice en ligne gratuite",
|
||||
"short_name": "Calculatrice",
|
||||
"description": "Calculatrice en ligne gratuite : mode simple et scientifique, historique, thème sombre.",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#f0f2f5",
|
||||
"theme_color": "#4f46e5",
|
||||
"orientation": "portrait-primary",
|
||||
"categories": ["utilities", "education"],
|
||||
"lang": "fr",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user